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.
4
5#include "chrome/browser/chromeos/first_run/drive_first_run_controller.h"
6
7#include "ash/shell.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"
47#include "url/gurl.h"
48
49namespace chromeos {
50
51namespace {
52
53// The initial time to wait in seconds before enabling offline mode.
54int kInitialDelaySeconds = 180;
55
56// Time to wait for Drive app background page to come up before giving up.
57int kWebContentsTimeoutSeconds = 15;
58
59// Google Drive enable offline endpoint.
60const char kDriveOfflineEndpointUrl[] =
61    "https://docs.google.com/offline/autoenable";
62
63// Google Drive app id.
64const char kDriveHostedAppId[] = "apdfllckaahabafndbhieahigkjlhalf";
65
66// Id of the notification shown when offline mode is enabled.
67const char kDriveOfflineNotificationId[] = "chrome://drive/enable-offline";
68
69// The URL of the support page opened when the notification button is clicked.
70const char kDriveOfflineSupportUrl[] =
71    "https://support.google.com/drive/answer/1628467";
72
73}  // namespace
74
75////////////////////////////////////////////////////////////////////////////////
76// DriveOfflineNotificationDelegate
77
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.
81class DriveOfflineNotificationDelegate
82    : public message_center::NotificationDelegate {
83 public:
84  explicit DriveOfflineNotificationDelegate(Profile* profile)
85      : profile_(profile) {}
86
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;
93
94 protected:
95  virtual ~DriveOfflineNotificationDelegate() {}
96
97 private:
98  Profile* profile_;
99
100  DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate);
101};
102
103void DriveOfflineNotificationDelegate::ButtonClick(int button_index) {
104  DCHECK_EQ(0, button_index);
105
106  // The support page will be localized based on the user's GAIA account.
107  const GURL url = GURL(kDriveOfflineSupportUrl);
108
109  chrome::ScopedTabbedBrowserDisplayer displayer(
110       profile_,
111       chrome::HOST_DESKTOP_TYPE_ASH);
112  chrome::ShowSingletonTabOverwritingNTP(
113      displayer.browser(),
114      chrome::GetSingletonTabNavigateParams(displayer.browser(), url));
115}
116
117////////////////////////////////////////////////////////////////////////////////
118// DriveWebContentsManager
119
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.
124class DriveWebContentsManager : public content::WebContentsObserver,
125                                public content::WebContentsDelegate,
126                                public content::NotificationObserver {
127 public:
128  typedef base::Callback<
129      void(bool, DriveFirstRunController::UMAOutcome)> CompletionCallback;
130
131  DriveWebContentsManager(Profile* profile,
132                          const std::string& app_id,
133                          const std::string& endpoint_url,
134                          const CompletionCallback& completion_callback);
135  virtual ~DriveWebContentsManager();
136
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.
139  void StartLoad();
140
141  // Stop loading the endpoint. The |completion_callback| will not be called.
142  void StopLoad();
143
144 private:
145  // Called when when offline initialization succeeds or fails and schedules
146  // |RunCompletionCallback|.
147  void OnOfflineInit(bool success,
148                     DriveFirstRunController::UMAOutcome outcome);
149
150  // Runs |completion_callback|.
151  void RunCompletionCallback(bool success,
152                             DriveFirstRunController::UMAOutcome outcome);
153
154  // content::WebContentsObserver overrides:
155  virtual void DidFailProvisionalLoad(
156      content::RenderFrameHost* render_frame_host,
157      const GURL& validated_url,
158      int error_code,
159      const base::string16& error_description) OVERRIDE;
160
161  virtual void DidFailLoad(content::RenderFrameHost* render_frame_host,
162                           const GURL& validated_url,
163                           int error_code,
164                           const base::string16& error_description) OVERRIDE;
165
166  // content::WebContentsDelegate overrides:
167  virtual bool ShouldCreateWebContents(
168      content::WebContents* web_contents,
169      int route_id,
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;
175
176  // content::NotificationObserver overrides:
177  virtual void Observe(int type,
178                       const content::NotificationSource& source,
179                       const content::NotificationDetails& details) OVERRIDE;
180
181  Profile* profile_;
182  const std::string app_id_;
183  const std::string endpoint_url_;
184  scoped_ptr<content::WebContents> web_contents_;
185  content::NotificationRegistrar registrar_;
186  bool started_;
187  CompletionCallback completion_callback_;
188  base::WeakPtrFactory<DriveWebContentsManager> weak_ptr_factory_;
189
190  DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager);
191};
192
193DriveWebContentsManager::DriveWebContentsManager(
194    Profile* profile,
195    const std::string& app_id,
196    const std::string& endpoint_url,
197    const CompletionCallback& completion_callback)
198    : profile_(profile),
199      app_id_(app_id),
200      endpoint_url_(endpoint_url),
201      started_(false),
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_));
207}
208
209DriveWebContentsManager::~DriveWebContentsManager() {
210}
211
212void DriveWebContentsManager::StartLoad() {
213  started_ = true;
214  const GURL url(endpoint_url_);
215  content::WebContents::CreateParams create_params(
216        profile_, content::SiteInstance::CreateForURL(profile_, url));
217
218  web_contents_.reset(content::WebContents::Create(create_params));
219  web_contents_->SetDelegate(this);
220  extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
221      web_contents_.get());
222
223  content::NavigationController::LoadURLParams load_params(url);
224  load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
225  web_contents_->GetController().LoadURLWithParams(load_params);
226
227  content::WebContentsObserver::Observe(web_contents_.get());
228}
229
230void DriveWebContentsManager::StopLoad() {
231  started_ = false;
232}
233
234void DriveWebContentsManager::OnOfflineInit(
235    bool success,
236    DriveFirstRunController::UMAOutcome outcome) {
237  if (started_) {
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(
241        FROM_HERE,
242        base::Bind(&DriveWebContentsManager::RunCompletionCallback,
243                   weak_ptr_factory_.GetWeakPtr(),
244                   success,
245                   outcome));
246    StopLoad();
247  }
248}
249
250void DriveWebContentsManager::RunCompletionCallback(
251    bool success,
252    DriveFirstRunController::UMAOutcome outcome) {
253  completion_callback_.Run(success, outcome);
254}
255
256void DriveWebContentsManager::DidFailProvisionalLoad(
257    content::RenderFrameHost* render_frame_host,
258    const GURL& validated_url,
259    int error_code,
260    const base::string16& error_description) {
261  if (!render_frame_host->GetParent()) {
262    LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
263    OnOfflineInit(false,
264                  DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
265  }
266}
267
268void DriveWebContentsManager::DidFailLoad(
269    content::RenderFrameHost* render_frame_host,
270    const GURL& validated_url,
271    int error_code,
272    const base::string16& error_description) {
273  if (!render_frame_host->GetParent()) {
274    LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
275    OnOfflineInit(false,
276                  DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
277  }
278}
279
280bool DriveWebContentsManager::ShouldCreateWebContents(
281    content::WebContents* web_contents,
282    int route_id,
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) {
288
289  if (window_container_type == WINDOW_CONTAINER_TYPE_NORMAL)
290    return true;
291
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_)
297    return true;
298
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_);
303
304  // Prevent redirection if background contents already exists.
305  if (background_contents_service->GetAppBackgroundContents(
306      base::UTF8ToUTF16(app_id_))) {
307    return false;
308  }
309  BackgroundContents* contents = background_contents_service
310      ->CreateBackgroundContents(content::SiteInstance::Create(profile_),
311                                 route_id,
312                                 profile_,
313                                 frame_name,
314                                 base::ASCIIToUTF16(app_id_),
315                                 partition_id,
316                                 session_storage_namespace);
317
318  contents->web_contents()->GetController().LoadURL(
319      target_url,
320      content::Referrer(),
321      ui::PAGE_TRANSITION_LINK,
322      std::string());
323
324  // Return false as we already created the WebContents here.
325  return false;
326}
327
328void DriveWebContentsManager::Observe(
329    int type,
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)
335            ->application_id);
336    if (app_id == app_id_)
337      OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED);
338  }
339}
340
341////////////////////////////////////////////////////////////////////////////////
342// DriveFirstRunController
343
344DriveFirstRunController::DriveFirstRunController(Profile* profile)
345    : profile_(profile),
346      started_(false),
347      initial_delay_secs_(kInitialDelaySeconds),
348      web_contents_timeout_secs_(kWebContentsTimeoutSeconds),
349      drive_offline_endpoint_url_(kDriveOfflineEndpointUrl),
350      drive_hosted_app_id_(kDriveHostedAppId) {
351}
352
353DriveFirstRunController::~DriveFirstRunController() {
354}
355
356void DriveFirstRunController::EnableOfflineMode() {
357  if (!started_) {
358    started_ = true;
359    initial_delay_timer_.Start(
360      FROM_HERE,
361      base::TimeDelta::FromSeconds(initial_delay_secs_),
362      this,
363      &DriveFirstRunController::EnableOfflineMode);
364    return;
365  }
366
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);
371    return;
372  }
373
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);
379    return;
380  }
381
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);
388    return;
389  }
390
391  web_contents_manager_.reset(new DriveWebContentsManager(
392      profile_,
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(
399      FROM_HERE,
400      base::TimeDelta::FromSeconds(web_contents_timeout_secs_),
401      this,
402      &DriveFirstRunController::OnWebContentsTimedOut);
403}
404
405void DriveFirstRunController::AddObserver(Observer* observer) {
406  observer_list_.AddObserver(observer);
407}
408
409void DriveFirstRunController::RemoveObserver(Observer* observer) {
410  observer_list_.RemoveObserver(observer);
411}
412
413void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs,
414                                               int timeout_secs) {
415  DCHECK(!started_);
416  initial_delay_secs_ = initial_delay_secs;
417  web_contents_timeout_secs_ = timeout_secs;
418}
419
420void DriveFirstRunController::SetAppInfoForTest(
421    const std::string& app_id,
422    const std::string& endpoint_url) {
423  DCHECK(!started_);
424  drive_hosted_app_id_ = app_id;
425  drive_offline_endpoint_url_ = endpoint_url;
426}
427
428void 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);
432}
433
434void 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);
439}
440
441void DriveFirstRunController::OnOfflineInit(bool success, UMAOutcome outcome) {
442  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
443  if (success)
444    ShowNotification();
445  UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
446                            outcome, OUTCOME_MAX);
447  FOR_EACH_OBSERVER(Observer, observer_list_, OnCompletion(success));
448  CleanUp();
449}
450
451void DriveFirstRunController::ShowNotification() {
452  ExtensionService* service =
453      extensions::ExtensionSystem::Get(profile_)->extension_service();
454  DCHECK(service);
455  const extensions::Extension* extension =
456      service->GetExtensionById(drive_hosted_app_id_, false);
457  DCHECK(extension);
458
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,
472                                     kDriveHostedAppId),
473          data,
474          new DriveOfflineNotificationDelegate(profile_)));
475  notification->set_priority(message_center::LOW_PRIORITY);
476  message_center::MessageCenter::Get()->AddNotification(notification.Pass());
477}
478
479}  // namespace chromeos
480