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/chromeos/login/users/user_manager.h"
18#include "chrome/browser/extensions/chrome_extension_web_contents_observer.h"
19#include "chrome/browser/extensions/extension_service.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_registry.h"
36#include "extensions/browser/extension_system.h"
37#include "extensions/common/extension.h"
38#include "extensions/common/extension_set.h"
39#include "grit/generated_resources.h"
40#include "grit/theme_resources.h"
41#include "ui/base/l10n/l10n_util.h"
42#include "ui/base/resource/resource_bundle.h"
43#include "ui/message_center/message_center.h"
44#include "ui/message_center/notification.h"
45#include "ui/message_center/notification_delegate.h"
46#include "url/gurl.h"
47
48namespace chromeos {
49
50namespace {
51
52// The initial time to wait in seconds before enabling offline mode.
53int kInitialDelaySeconds = 180;
54
55// Time to wait for Drive app background page to come up before giving up.
56int kWebContentsTimeoutSeconds = 15;
57
58// Google Drive enable offline endpoint.
59const char kDriveOfflineEndpointUrl[] =
60    "https://docs.google.com/offline/autoenable";
61
62// Google Drive app id.
63const char kDriveHostedAppId[] = "apdfllckaahabafndbhieahigkjlhalf";
64
65// Id of the notification shown when offline mode is enabled.
66const char kDriveOfflineNotificationId[] = "chrome://drive/enable-offline";
67
68// The URL of the support page opened when the notification button is clicked.
69const char kDriveOfflineSupportUrl[] =
70    "https://support.google.com/drive/answer/1628467";
71
72}  // namespace
73
74////////////////////////////////////////////////////////////////////////////////
75// DriveOfflineNotificationDelegate
76
77// NotificationDelegate for the notification that is displayed when Drive
78// offline mode is enabled automatically. Clicking on the notification button
79// will open the Drive settings page.
80class DriveOfflineNotificationDelegate
81    : public message_center::NotificationDelegate {
82 public:
83  explicit DriveOfflineNotificationDelegate(Profile* profile)
84      : profile_(profile) {}
85
86  // message_center::NotificationDelegate overrides:
87  virtual void Display() OVERRIDE {}
88  virtual void Error() OVERRIDE {}
89  virtual void Close(bool by_user) OVERRIDE {}
90  virtual void Click() OVERRIDE {}
91  virtual void ButtonClick(int button_index) OVERRIDE;
92
93 protected:
94  virtual ~DriveOfflineNotificationDelegate() {}
95
96 private:
97  Profile* profile_;
98
99  DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate);
100};
101
102void DriveOfflineNotificationDelegate::ButtonClick(int button_index) {
103  DCHECK_EQ(0, button_index);
104
105  // The support page will be localized based on the user's GAIA account.
106  const GURL url = GURL(kDriveOfflineSupportUrl);
107
108  chrome::ScopedTabbedBrowserDisplayer displayer(
109       profile_,
110       chrome::HOST_DESKTOP_TYPE_ASH);
111  chrome::ShowSingletonTabOverwritingNTP(
112      displayer.browser(),
113      chrome::GetSingletonTabNavigateParams(displayer.browser(), url));
114}
115
116////////////////////////////////////////////////////////////////////////////////
117// DriveWebContentsManager
118
119// Manages web contents that initializes Google Drive offline mode. We create
120// a background WebContents that loads a Drive endpoint to initialize offline
121// mode. If successful, a background page will be opened to sync the user's
122// files for offline use.
123class DriveWebContentsManager : public content::WebContentsObserver,
124                                public content::WebContentsDelegate,
125                                public content::NotificationObserver {
126 public:
127  typedef base::Callback<
128      void(bool, DriveFirstRunController::UMAOutcome)> CompletionCallback;
129
130  DriveWebContentsManager(Profile* profile,
131                          const std::string& app_id,
132                          const std::string& endpoint_url,
133                          const CompletionCallback& completion_callback);
134  virtual ~DriveWebContentsManager();
135
136  // Start loading the WebContents for the endpoint in the context of the Drive
137  // hosted app that will initialize offline mode and open a background page.
138  void StartLoad();
139
140  // Stop loading the endpoint. The |completion_callback| will not be called.
141  void StopLoad();
142
143 private:
144  // Called when when offline initialization succeeds or fails and schedules
145  // |RunCompletionCallback|.
146  void OnOfflineInit(bool success,
147                     DriveFirstRunController::UMAOutcome outcome);
148
149  // Runs |completion_callback|.
150  void RunCompletionCallback(bool success,
151                             DriveFirstRunController::UMAOutcome outcome);
152
153  // content::WebContentsObserver overrides:
154  virtual void DidFailProvisionalLoad(
155      int64 frame_id,
156      const base::string16& frame_unique_name,
157      bool is_main_frame,
158      const GURL& validated_url,
159      int error_code,
160      const base::string16& error_description,
161      content::RenderViewHost* render_view_host) OVERRIDE;
162
163  virtual void DidFailLoad(int64 frame_id,
164                           const GURL& validated_url,
165                           bool is_main_frame,
166                           int error_code,
167                           const base::string16& error_description,
168                           content::RenderViewHost* render_view_host) OVERRIDE;
169
170  // content::WebContentsDelegate overrides:
171  virtual bool ShouldCreateWebContents(
172      content::WebContents* web_contents,
173      int route_id,
174      WindowContainerType window_container_type,
175      const base::string16& frame_name,
176      const GURL& target_url,
177      const std::string& partition_id,
178      content::SessionStorageNamespace* session_storage_namespace) OVERRIDE;
179
180  // content::NotificationObserver overrides:
181  virtual void Observe(int type,
182                       const content::NotificationSource& source,
183                       const content::NotificationDetails& details) OVERRIDE;
184
185  Profile* profile_;
186  const std::string app_id_;
187  const std::string endpoint_url_;
188  scoped_ptr<content::WebContents> web_contents_;
189  content::NotificationRegistrar registrar_;
190  bool started_;
191  CompletionCallback completion_callback_;
192  base::WeakPtrFactory<DriveWebContentsManager> weak_ptr_factory_;
193
194  DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager);
195};
196
197DriveWebContentsManager::DriveWebContentsManager(
198    Profile* profile,
199    const std::string& app_id,
200    const std::string& endpoint_url,
201    const CompletionCallback& completion_callback)
202    : profile_(profile),
203      app_id_(app_id),
204      endpoint_url_(endpoint_url),
205      started_(false),
206      completion_callback_(completion_callback),
207      weak_ptr_factory_(this) {
208  DCHECK(!completion_callback_.is_null());
209  registrar_.Add(this, chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED,
210                 content::Source<Profile>(profile_));
211}
212
213DriveWebContentsManager::~DriveWebContentsManager() {
214}
215
216void DriveWebContentsManager::StartLoad() {
217  started_ = true;
218  const GURL url(endpoint_url_);
219  content::WebContents::CreateParams create_params(
220        profile_, content::SiteInstance::CreateForURL(profile_, url));
221
222  web_contents_.reset(content::WebContents::Create(create_params));
223  web_contents_->SetDelegate(this);
224  extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
225      web_contents_.get());
226
227  content::NavigationController::LoadURLParams load_params(url);
228  load_params.transition_type = content::PAGE_TRANSITION_GENERATED;
229  web_contents_->GetController().LoadURLWithParams(load_params);
230
231  content::WebContentsObserver::Observe(web_contents_.get());
232}
233
234void DriveWebContentsManager::StopLoad() {
235  started_ = false;
236}
237
238void DriveWebContentsManager::OnOfflineInit(
239    bool success,
240    DriveFirstRunController::UMAOutcome outcome) {
241  if (started_) {
242    // We postpone notifying the controller as we may be in the middle
243    // of a call stack for some routine of the contained WebContents.
244    base::MessageLoop::current()->PostTask(
245        FROM_HERE,
246        base::Bind(&DriveWebContentsManager::RunCompletionCallback,
247                   weak_ptr_factory_.GetWeakPtr(),
248                   success,
249                   outcome));
250    StopLoad();
251  }
252}
253
254void DriveWebContentsManager::RunCompletionCallback(
255    bool success,
256    DriveFirstRunController::UMAOutcome outcome) {
257  completion_callback_.Run(success, outcome);
258}
259
260void DriveWebContentsManager::DidFailProvisionalLoad(
261    int64 frame_id,
262    const base::string16& frame_unique_name,
263    bool is_main_frame,
264    const GURL& validated_url,
265    int error_code,
266    const base::string16& error_description,
267    content::RenderViewHost* render_view_host) {
268  if (is_main_frame) {
269    LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
270    OnOfflineInit(false,
271                  DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
272  }
273}
274
275void DriveWebContentsManager::DidFailLoad(
276    int64 frame_id,
277    const GURL& validated_url,
278    bool is_main_frame,
279    int error_code,
280    const base::string16& error_description,
281    content::RenderViewHost* render_view_host) {
282  if (is_main_frame) {
283    LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
284    OnOfflineInit(false,
285                  DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
286  }
287}
288
289bool DriveWebContentsManager::ShouldCreateWebContents(
290    content::WebContents* web_contents,
291    int route_id,
292    WindowContainerType window_container_type,
293    const base::string16& frame_name,
294    const GURL& target_url,
295    const std::string& partition_id,
296    content::SessionStorageNamespace* session_storage_namespace) {
297
298  if (window_container_type == WINDOW_CONTAINER_TYPE_NORMAL)
299    return true;
300
301  // Check that the target URL is for the Drive app.
302  const extensions::Extension* extension =
303      extensions::ExtensionRegistry::Get(profile_)
304          ->enabled_extensions().GetAppByURL(target_url);
305  if (!extension || extension->id() != app_id_)
306    return true;
307
308  // The background contents creation is normally done in Browser, but
309  // because we're using a detached WebContents, we need to do it ourselves.
310  BackgroundContentsService* background_contents_service =
311      BackgroundContentsServiceFactory::GetForProfile(profile_);
312
313  // Prevent redirection if background contents already exists.
314  if (background_contents_service->GetAppBackgroundContents(
315      base::UTF8ToUTF16(app_id_))) {
316    return false;
317  }
318  BackgroundContents* contents = background_contents_service
319      ->CreateBackgroundContents(content::SiteInstance::Create(profile_),
320                                 route_id,
321                                 profile_,
322                                 frame_name,
323                                 base::ASCIIToUTF16(app_id_),
324                                 partition_id,
325                                 session_storage_namespace);
326
327  contents->web_contents()->GetController().LoadURL(
328      target_url,
329      content::Referrer(),
330      content::PAGE_TRANSITION_LINK,
331      std::string());
332
333  // Return false as we already created the WebContents here.
334  return false;
335}
336
337void DriveWebContentsManager::Observe(
338    int type,
339    const content::NotificationSource& source,
340    const content::NotificationDetails& details) {
341  if (type == chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED) {
342    const std::string app_id = base::UTF16ToUTF8(
343        content::Details<BackgroundContentsOpenedDetails>(details)
344            ->application_id);
345    if (app_id == app_id_)
346      OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED);
347  }
348}
349
350////////////////////////////////////////////////////////////////////////////////
351// DriveFirstRunController
352
353DriveFirstRunController::DriveFirstRunController(Profile* profile)
354    : profile_(profile),
355      started_(false),
356      initial_delay_secs_(kInitialDelaySeconds),
357      web_contents_timeout_secs_(kWebContentsTimeoutSeconds),
358      drive_offline_endpoint_url_(kDriveOfflineEndpointUrl),
359      drive_hosted_app_id_(kDriveHostedAppId) {
360}
361
362DriveFirstRunController::~DriveFirstRunController() {
363}
364
365void DriveFirstRunController::EnableOfflineMode() {
366  if (!started_) {
367    started_ = true;
368    initial_delay_timer_.Start(
369      FROM_HERE,
370      base::TimeDelta::FromSeconds(initial_delay_secs_),
371      this,
372      &DriveFirstRunController::EnableOfflineMode);
373    return;
374  }
375
376  if (!UserManager::Get()->IsLoggedInAsRegularUser()) {
377    LOG(ERROR) << "Attempting to enable offline access "
378                  "but not logged in a regular user.";
379    OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE);
380    return;
381  }
382
383  ExtensionService* extension_service =
384      extensions::ExtensionSystem::Get(profile_)->extension_service();
385  if (!extension_service->GetExtensionById(drive_hosted_app_id_, false)) {
386    LOG(WARNING) << "Drive app is not installed.";
387    OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED);
388    return;
389  }
390
391  BackgroundContentsService* background_contents_service =
392      BackgroundContentsServiceFactory::GetForProfile(profile_);
393  if (background_contents_service->GetAppBackgroundContents(
394      base::UTF8ToUTF16(drive_hosted_app_id_))) {
395    LOG(WARNING) << "Background page for Drive app already exists";
396    OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS);
397    return;
398  }
399
400  web_contents_manager_.reset(new DriveWebContentsManager(
401      profile_,
402      drive_hosted_app_id_,
403      drive_offline_endpoint_url_,
404      base::Bind(&DriveFirstRunController::OnOfflineInit,
405                 base::Unretained(this))));
406  web_contents_manager_->StartLoad();
407  web_contents_timer_.Start(
408      FROM_HERE,
409      base::TimeDelta::FromSeconds(web_contents_timeout_secs_),
410      this,
411      &DriveFirstRunController::OnWebContentsTimedOut);
412}
413
414void DriveFirstRunController::AddObserver(Observer* observer) {
415  observer_list_.AddObserver(observer);
416}
417
418void DriveFirstRunController::RemoveObserver(Observer* observer) {
419  observer_list_.RemoveObserver(observer);
420}
421
422void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs,
423                                               int timeout_secs) {
424  DCHECK(!started_);
425  initial_delay_secs_ = initial_delay_secs;
426  web_contents_timeout_secs_ = timeout_secs;
427}
428
429void DriveFirstRunController::SetAppInfoForTest(
430    const std::string& app_id,
431    const std::string& endpoint_url) {
432  DCHECK(!started_);
433  drive_hosted_app_id_ = app_id;
434  drive_offline_endpoint_url_ = endpoint_url;
435}
436
437void DriveFirstRunController::OnWebContentsTimedOut() {
438  LOG(WARNING) << "Timed out waiting for web contents.";
439  FOR_EACH_OBSERVER(Observer, observer_list_, OnTimedOut());
440  OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT);
441}
442
443void DriveFirstRunController::CleanUp() {
444  if (web_contents_manager_)
445    web_contents_manager_->StopLoad();
446  web_contents_timer_.Stop();
447  base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
448}
449
450void DriveFirstRunController::OnOfflineInit(bool success, UMAOutcome outcome) {
451  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
452  if (success)
453    ShowNotification();
454  UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
455                            outcome, OUTCOME_MAX);
456  FOR_EACH_OBSERVER(Observer, observer_list_, OnCompletion(success));
457  CleanUp();
458}
459
460void DriveFirstRunController::ShowNotification() {
461  ExtensionService* service =
462      extensions::ExtensionSystem::Get(profile_)->extension_service();
463  DCHECK(service);
464  const extensions::Extension* extension =
465      service->GetExtensionById(drive_hosted_app_id_, false);
466  DCHECK(extension);
467
468  message_center::RichNotificationData data;
469  data.buttons.push_back(message_center::ButtonInfo(
470      l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON)));
471  ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
472  scoped_ptr<message_center::Notification> notification(
473      new message_center::Notification(
474          message_center::NOTIFICATION_TYPE_SIMPLE,
475          kDriveOfflineNotificationId,
476          base::string16(), // title
477          l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_MESSAGE),
478          resource_bundle.GetImageNamed(IDR_NOTIFICATION_DRIVE),
479          base::UTF8ToUTF16(extension->name()),
480          message_center::NotifierId(message_center::NotifierId::APPLICATION,
481                                     kDriveHostedAppId),
482          data,
483          new DriveOfflineNotificationDelegate(profile_)));
484  notification->set_priority(message_center::LOW_PRIORITY);
485  message_center::MessageCenter::Get()->AddNotification(notification.Pass());
486}
487
488}  // namespace chromeos
489