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:
DriveOfflineNotificationDelegate(Profile * profile)83 explicit DriveOfflineNotificationDelegate(Profile* profile)
84 : profile_(profile) {}
85
86 // message_center::NotificationDelegate overrides:
Display()87 virtual void Display() OVERRIDE {}
Error()88 virtual void Error() OVERRIDE {}
Close(bool by_user)89 virtual void Close(bool by_user) OVERRIDE {}
Click()90 virtual void Click() OVERRIDE {}
91 virtual void ButtonClick(int button_index) OVERRIDE;
92
93 protected:
~DriveOfflineNotificationDelegate()94 virtual ~DriveOfflineNotificationDelegate() {}
95
96 private:
97 Profile* profile_;
98
99 DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate);
100 };
101
ButtonClick(int button_index)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
DriveWebContentsManager(Profile * profile,const std::string & app_id,const std::string & endpoint_url,const CompletionCallback & completion_callback)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
~DriveWebContentsManager()213 DriveWebContentsManager::~DriveWebContentsManager() {
214 }
215
StartLoad()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
StopLoad()234 void DriveWebContentsManager::StopLoad() {
235 started_ = false;
236 }
237
OnOfflineInit(bool success,DriveFirstRunController::UMAOutcome outcome)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
RunCompletionCallback(bool success,DriveFirstRunController::UMAOutcome outcome)254 void DriveWebContentsManager::RunCompletionCallback(
255 bool success,
256 DriveFirstRunController::UMAOutcome outcome) {
257 completion_callback_.Run(success, outcome);
258 }
259
DidFailProvisionalLoad(int64 frame_id,const base::string16 & frame_unique_name,bool is_main_frame,const GURL & validated_url,int error_code,const base::string16 & error_description,content::RenderViewHost * render_view_host)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
DidFailLoad(int64 frame_id,const GURL & validated_url,bool is_main_frame,int error_code,const base::string16 & error_description,content::RenderViewHost * render_view_host)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
ShouldCreateWebContents(content::WebContents * web_contents,int route_id,WindowContainerType window_container_type,const base::string16 & frame_name,const GURL & target_url,const std::string & partition_id,content::SessionStorageNamespace * session_storage_namespace)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
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)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
DriveFirstRunController(Profile * profile)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
~DriveFirstRunController()362 DriveFirstRunController::~DriveFirstRunController() {
363 }
364
EnableOfflineMode()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
AddObserver(Observer * observer)414 void DriveFirstRunController::AddObserver(Observer* observer) {
415 observer_list_.AddObserver(observer);
416 }
417
RemoveObserver(Observer * observer)418 void DriveFirstRunController::RemoveObserver(Observer* observer) {
419 observer_list_.RemoveObserver(observer);
420 }
421
SetDelaysForTest(int initial_delay_secs,int timeout_secs)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
SetAppInfoForTest(const std::string & app_id,const std::string & endpoint_url)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
OnWebContentsTimedOut()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
CleanUp()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
OnOfflineInit(bool success,UMAOutcome outcome)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
ShowNotification()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