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