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