blob: 1eb8a9539eeaf991cf7f3931bfb675acd5418d58 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/first_run/drive_first_run_controller.h"
#include <stdint.h>
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/background/background_contents.h"
#include "chrome/browser/background/background_contents_service.h"
#include "chrome/browser/background/background_contents_service_factory.h"
#include "chrome/browser/background/background_contents_service_observer.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/chrome_extension_web_contents_observer.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/window_container_type.mojom-shared.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_set.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
#include "url/gurl.h"
namespace ash {
namespace {
// The initial time to wait in seconds before enabling offline mode.
int kInitialDelaySeconds = 180;
// Time to wait for Drive app background page to come up before giving up.
int kWebContentsTimeoutSeconds = 15;
// Google Drive enable offline endpoint.
const char kDriveOfflineEndpointUrl[] =
"https://docs.google.com/offline/autoenable";
// Google Drive app id.
const char kDriveHostedAppId[] = "apdfllckaahabafndbhieahigkjlhalf";
// Id of the notification shown when offline mode is enabled.
const char kDriveOfflineNotificationId[] = "chrome://drive/enable-offline";
// The URL of the support page opened when the notification button is clicked.
const char kDriveOfflineSupportUrl[] =
"https://support.google.com/drive/answer/1628467";
} // namespace
////////////////////////////////////////////////////////////////////////////////
// DriveWebContentsManager
// Manages web contents that initializes Google Drive offline mode. We create
// a background WebContents that loads a Drive endpoint to initialize offline
// mode. If successful, a background page will be opened to sync the user's
// files for offline use.
class DriveWebContentsManager : public content::WebContentsObserver,
public content::WebContentsDelegate,
public BackgroundContentsServiceObserver {
public:
using CompletionCallback =
base::OnceCallback<void(bool, DriveFirstRunController::UMAOutcome)>;
DriveWebContentsManager(Profile* profile,
const std::string& app_id,
const std::string& endpoint_url,
CompletionCallback completion_callback);
~DriveWebContentsManager() override;
// Start loading the WebContents for the endpoint in the context of the Drive
// hosted app that will initialize offline mode and open a background page.
void StartLoad();
// Stop loading the endpoint. The |completion_callback| will not be called.
void StopLoad();
private:
// Called when when offline initialization succeeds or fails and schedules
// |RunCompletionCallback|.
void OnOfflineInit(bool success,
DriveFirstRunController::UMAOutcome outcome);
// Runs |completion_callback|.
void RunCompletionCallback(bool success,
DriveFirstRunController::UMAOutcome outcome);
// content::WebContentsObserver overrides:
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override;
void DidFailLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url,
int error_code) override;
// content::WebContentsDelegate overrides:
bool IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url) override;
content::WebContents* CreateCustomWebContents(
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
bool is_new_browsing_instance,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url,
const content::StoragePartitionId& partition_id,
content::SessionStorageNamespace* session_storage_namespace) override;
// BackgroundContentsServiceObserver:
void OnBackgroundContentsOpened(
const BackgroundContentsOpenedDetails& details) override;
Profile* profile_;
const std::string app_id_;
const std::string endpoint_url_;
std::unique_ptr<content::WebContents> web_contents_;
bool started_ = false;
CompletionCallback completion_callback_;
base::WeakPtrFactory<DriveWebContentsManager> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager);
};
DriveWebContentsManager::DriveWebContentsManager(
Profile* profile,
const std::string& app_id,
const std::string& endpoint_url,
CompletionCallback completion_callback)
: profile_(profile),
app_id_(app_id),
endpoint_url_(endpoint_url),
completion_callback_(std::move(completion_callback)) {
DCHECK(!completion_callback_.is_null());
BackgroundContentsServiceFactory::GetForProfile(profile)->AddObserver(this);
}
DriveWebContentsManager::~DriveWebContentsManager() {
BackgroundContentsServiceFactory::GetForProfile(profile_)->RemoveObserver(
this);
}
void DriveWebContentsManager::StartLoad() {
started_ = true;
const GURL url(endpoint_url_);
content::WebContents::CreateParams create_params(
profile_, content::SiteInstance::CreateForURL(profile_, url));
web_contents_ = content::WebContents::Create(create_params);
web_contents_->SetDelegate(this);
extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
web_contents_.get());
content::NavigationController::LoadURLParams load_params(url);
load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
web_contents_->GetController().LoadURLWithParams(load_params);
content::WebContentsObserver::Observe(web_contents_.get());
}
void DriveWebContentsManager::StopLoad() {
started_ = false;
}
void DriveWebContentsManager::OnOfflineInit(
bool success,
DriveFirstRunController::UMAOutcome outcome) {
if (started_) {
// We postpone notifying the controller as we may be in the middle
// of a call stack for some routine of the contained WebContents.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&DriveWebContentsManager::RunCompletionCallback,
weak_ptr_factory_.GetWeakPtr(), success, outcome));
StopLoad();
}
}
void DriveWebContentsManager::RunCompletionCallback(
bool success,
DriveFirstRunController::UMAOutcome outcome) {
std::move(completion_callback_).Run(success, outcome);
}
void DriveWebContentsManager::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// TODO(https://crbug.com/1218946): With MPArch there may be multiple main
// frames. This caller was converted automatically to the primary main frame
// to preserve its semantics. Follow up to confirm correctness.
if (navigation_handle->IsInPrimaryMainFrame() &&
navigation_handle->IsErrorPage()) {
LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
OnOfflineInit(false,
DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
}
}
void DriveWebContentsManager::DidFailLoad(
content::RenderFrameHost* render_frame_host,
const GURL& validated_url,
int error_code) {
if (!render_frame_host->GetParent()) {
LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
OnOfflineInit(false,
DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
}
}
bool DriveWebContentsManager::IsWebContentsCreationOverridden(
content::SiteInstance* source_site_instance,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url) {
if (window_container_type == content::mojom::WindowContainerType::NORMAL)
return false;
// Check that the target URL is for the Drive app.
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(profile_)
->enabled_extensions().GetAppByURL(target_url);
return extension && extension->id() == app_id_;
}
content::WebContents* DriveWebContentsManager::CreateCustomWebContents(
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
bool is_new_browsing_instance,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url,
const content::StoragePartitionId& partition_id,
content::SessionStorageNamespace* session_storage_namespace) {
// The background contents creation is normally done in Browser, but
// because we're using a detached WebContents, we need to do it ourselves.
BackgroundContentsService* background_contents_service =
BackgroundContentsServiceFactory::GetForProfile(profile_);
// Only redirect if background contents does not yet exists.
if (!background_contents_service->GetAppBackgroundContents(app_id_)) {
// drive_first_run/app/manifest.json sets allow_js_access to false and
// therefore we are creating a new SiteInstance (and thus a new renderer
// process) here, so we must use MSG_ROUTING_NONE and we cannot pass the
// opener (similarly to how allow_js_access:false is handled in
// Browser::MaybeCreateBackgroundContents).
BackgroundContents* contents =
background_contents_service->CreateBackgroundContents(
content::SiteInstance::Create(profile_), nullptr, true, frame_name,
app_id_, partition_id, session_storage_namespace);
contents->web_contents()->GetController().LoadURL(
target_url, content::Referrer(), ui::PAGE_TRANSITION_LINK,
std::string());
}
return nullptr;
}
void DriveWebContentsManager::OnBackgroundContentsOpened(
const BackgroundContentsOpenedDetails& details) {
if (details.application_id == app_id_)
OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED);
}
////////////////////////////////////////////////////////////////////////////////
// DriveFirstRunController
DriveFirstRunController::DriveFirstRunController(Profile* profile)
: profile_(profile),
started_(false),
initial_delay_secs_(kInitialDelaySeconds),
web_contents_timeout_secs_(kWebContentsTimeoutSeconds),
drive_offline_endpoint_url_(kDriveOfflineEndpointUrl),
drive_hosted_app_id_(kDriveHostedAppId) {
}
DriveFirstRunController::~DriveFirstRunController() {
}
void DriveFirstRunController::EnableOfflineMode() {
if (!started_) {
started_ = true;
initial_delay_timer_.Start(
FROM_HERE,
base::TimeDelta::FromSeconds(initial_delay_secs_),
this,
&DriveFirstRunController::EnableOfflineMode);
return;
}
if (!user_manager::UserManager::Get()->IsLoggedInAsUserWithGaiaAccount()) {
LOG(ERROR) << "Attempting to enable offline access "
"but not logged in a regular user.";
OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE);
return;
}
extensions::ExtensionRegistry* extension_registry =
extensions::ExtensionRegistry::Get(profile_);
if (!extension_registry->GetExtensionById(
drive_hosted_app_id_, extensions::ExtensionRegistry::ENABLED)) {
LOG(WARNING) << "Drive app is not installed.";
OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED);
return;
}
BackgroundContentsService* background_contents_service =
BackgroundContentsServiceFactory::GetForProfile(profile_);
if (background_contents_service->GetAppBackgroundContents(
drive_hosted_app_id_)) {
LOG(WARNING) << "Background page for Drive app already exists";
OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS);
return;
}
web_contents_manager_ = std::make_unique<DriveWebContentsManager>(
profile_, drive_hosted_app_id_, drive_offline_endpoint_url_,
base::BindOnce(&DriveFirstRunController::OnOfflineInit,
base::Unretained(this)));
web_contents_manager_->StartLoad();
web_contents_timer_.Start(
FROM_HERE,
base::TimeDelta::FromSeconds(web_contents_timeout_secs_),
this,
&DriveFirstRunController::OnWebContentsTimedOut);
}
void DriveFirstRunController::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void DriveFirstRunController::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs,
int timeout_secs) {
DCHECK(!started_);
initial_delay_secs_ = initial_delay_secs;
web_contents_timeout_secs_ = timeout_secs;
}
void DriveFirstRunController::SetAppInfoForTest(
const std::string& app_id,
const std::string& endpoint_url) {
DCHECK(!started_);
drive_hosted_app_id_ = app_id;
drive_offline_endpoint_url_ = endpoint_url;
}
void DriveFirstRunController::OnWebContentsTimedOut() {
LOG(WARNING) << "Timed out waiting for web contents.";
for (auto& observer : observer_list_)
observer.OnTimedOut();
OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT);
}
void DriveFirstRunController::CleanUp() {
if (web_contents_manager_)
web_contents_manager_->StopLoad();
web_contents_timer_.Stop();
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
}
void DriveFirstRunController::OnOfflineInit(bool success, UMAOutcome outcome) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (success)
ShowNotification();
UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
outcome, OUTCOME_MAX);
for (auto& observer : observer_list_)
observer.OnCompletion(success);
CleanUp();
}
void DriveFirstRunController::ShowNotification() {
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile_);
DCHECK(registry);
const extensions::Extension* extension = registry->GetExtensionById(
drive_hosted_app_id_, extensions::ExtensionRegistry::ENABLED);
DCHECK(extension);
message_center::RichNotificationData data;
data.buttons.push_back(message_center::ButtonInfo(
l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON)));
ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
// Clicking on the notification button will open the Drive settings page.
auto delegate =
base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
base::BindRepeating(
[](Profile* profile, absl::optional<int> button_index) {
if (!button_index)
return;
DCHECK_EQ(0, *button_index);
// The support page will be localized based on the user's GAIA
// account.
const GURL url = GURL(kDriveOfflineSupportUrl);
chrome::ScopedTabbedBrowserDisplayer displayer(profile);
NavigateParams params(
GetSingletonTabNavigateParams(displayer.browser(), url));
ShowSingletonTabOverwritingNTP(displayer.browser(), &params);
},
profile_));
message_center::Notification notification(
message_center::NOTIFICATION_TYPE_SIMPLE, kDriveOfflineNotificationId,
std::u16string(), // title
l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_MESSAGE),
resource_bundle.GetImageNamed(IDR_NOTIFICATION_DRIVE),
base::UTF8ToUTF16(extension->name()), GURL(),
message_center::NotifierId(message_center::NotifierType::APPLICATION,
kDriveHostedAppId),
data, std::move(delegate));
notification.set_priority(message_center::LOW_PRIORITY);
NotificationDisplayService::GetForProfile(profile_)->Display(
NotificationHandler::Type::TRANSIENT, notification, /*metadata=*/nullptr);
}
} // namespace ash