Home | History | Annotate | Download | only in first_run
      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 
     48 namespace chromeos {
     49 
     50 namespace {
     51 
     52 // The initial time to wait in seconds before enabling offline mode.
     53 int kInitialDelaySeconds = 180;
     54 
     55 // Time to wait for Drive app background page to come up before giving up.
     56 int kWebContentsTimeoutSeconds = 15;
     57 
     58 // Google Drive enable offline endpoint.
     59 const char kDriveOfflineEndpointUrl[] =
     60     "https://docs.google.com/offline/autoenable";
     61 
     62 // Google Drive app id.
     63 const char kDriveHostedAppId[] = "apdfllckaahabafndbhieahigkjlhalf";
     64 
     65 // Id of the notification shown when offline mode is enabled.
     66 const char kDriveOfflineNotificationId[] = "chrome://drive/enable-offline";
     67 
     68 // The URL of the support page opened when the notification button is clicked.
     69 const 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.
     80 class 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 
    102 void 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.
    123 class 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 
    197 DriveWebContentsManager::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 
    213 DriveWebContentsManager::~DriveWebContentsManager() {
    214 }
    215 
    216 void 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 
    234 void DriveWebContentsManager::StopLoad() {
    235   started_ = false;
    236 }
    237 
    238 void 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 
    254 void DriveWebContentsManager::RunCompletionCallback(
    255     bool success,
    256     DriveFirstRunController::UMAOutcome outcome) {
    257   completion_callback_.Run(success, outcome);
    258 }
    259 
    260 void 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 
    275 void 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 
    289 bool 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 
    337 void 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 
    353 DriveFirstRunController::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 
    362 DriveFirstRunController::~DriveFirstRunController() {
    363 }
    364 
    365 void 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 
    414 void DriveFirstRunController::AddObserver(Observer* observer) {
    415   observer_list_.AddObserver(observer);
    416 }
    417 
    418 void DriveFirstRunController::RemoveObserver(Observer* observer) {
    419   observer_list_.RemoveObserver(observer);
    420 }
    421 
    422 void 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 
    429 void 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 
    437 void 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 
    443 void 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 
    450 void 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 
    460 void 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