blob: c0a08a794025a48f813a0397b70a40657ff4a565 [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/extensions/api/tabs/tabs_event_router.h"
#include <stddef.h>
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/tabs/tabs_constants.h"
#include "chrome/browser/extensions/api/tabs/tabs_windows_api.h"
#include "chrome/browser/extensions/api/tabs/windows_event_router.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/extensions/extension_constants.h"
#include "components/favicon/content/content_favicon_driver.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
using base::DictionaryValue;
using base::ListValue;
using base::FundamentalValue;
using content::WebContents;
using zoom::ZoomController;
namespace extensions {
namespace {
namespace tabs = api::tabs;
bool WillDispatchTabUpdatedEvent(
WebContents* contents,
const std::set<std::string> changed_property_names,
content::BrowserContext* context,
const Extension* extension,
Event* event,
const base::DictionaryValue* listener_filter) {
std::unique_ptr<api::tabs::Tab> tab_object =
ExtensionTabUtil::CreateTabObject(contents, extension);
base::DictionaryValue* tab_value = tab_object->ToValue().release();
std::unique_ptr<base::DictionaryValue> changed_properties(
new base::DictionaryValue);
const base::Value* value = nullptr;
for (const auto& property : changed_property_names) {
if (tab_value->Get(property, &value))
changed_properties->Set(property, base::WrapUnique(value->DeepCopy()));
}
event->event_args->Set(1, changed_properties.release());
event->event_args->Set(2, tab_value);
return true;
}
} // namespace
TabsEventRouter::TabEntry::TabEntry(TabsEventRouter* router,
content::WebContents* contents)
: WebContentsObserver(contents),
complete_waiting_on_load_(false),
was_audible_(contents->WasRecentlyAudible()),
was_muted_(contents->IsAudioMuted()),
router_(router) {}
std::set<std::string> TabsEventRouter::TabEntry::UpdateLoadState() {
// The tab may go in & out of loading (for instance if iframes navigate).
// We only want to respond to the first change from loading to !loading after
// the NavigationEntryCommitted() was fired.
if (!complete_waiting_on_load_ || web_contents()->IsLoading()) {
return std::set<std::string>();
}
// Send 'status' of tab change. Expecting 'complete' is fired.
complete_waiting_on_load_ = false;
std::set<std::string> changed_property_names;
changed_property_names.insert(tabs_constants::kStatusKey);
return changed_property_names;
}
bool TabsEventRouter::TabEntry::SetAudible(bool new_val) {
if (was_audible_ == new_val)
return false;
was_audible_ = new_val;
return true;
}
bool TabsEventRouter::TabEntry::SetMuted(bool new_val) {
if (was_muted_ == new_val)
return false;
was_muted_ = new_val;
return true;
}
void TabsEventRouter::TabEntry::NavigationEntryCommitted(
const content::LoadCommittedDetails& load_details) {
// Send 'status' of tab change. Expecting 'loading' is fired.
complete_waiting_on_load_ = true;
std::set<std::string> changed_property_names;
changed_property_names.insert(tabs_constants::kStatusKey);
if (web_contents()->GetURL() != url_) {
url_ = web_contents()->GetURL();
changed_property_names.insert(tabs_constants::kUrlKey);
}
router_->TabUpdated(this, std::move(changed_property_names));
}
void TabsEventRouter::TabEntry::TitleWasSet(content::NavigationEntry* entry,
bool explicit_set) {
std::set<std::string> changed_property_names;
changed_property_names.insert(tabs_constants::kTitleKey);
router_->TabUpdated(this, std::move(changed_property_names));
}
void TabsEventRouter::TabEntry::WebContentsDestroyed() {
// This is necessary because it's possible for tabs to be created, detached
// and then destroyed without ever having been re-attached and closed. This
// happens in the case of a devtools WebContents that is opened in window,
// docked, then closed.
// Warning: |this| will be deleted after this call.
router_->UnregisterForTabNotifications(web_contents());
}
TabsEventRouter::TabsEventRouter(Profile* profile)
: profile_(profile),
favicon_scoped_observer_(this),
browser_tab_strip_tracker_(this, this, this) {
DCHECK(!profile->IsOffTheRecord());
browser_tab_strip_tracker_.Init(
BrowserTabStripTracker::InitWith::ALL_BROWERS);
}
TabsEventRouter::~TabsEventRouter() {
}
bool TabsEventRouter::ShouldTrackBrowser(Browser* browser) {
return profile_->IsSameProfile(browser->profile()) &&
ExtensionTabUtil::BrowserSupportsTabs(browser);
}
void TabsEventRouter::RegisterForTabNotifications(WebContents* contents) {
favicon_scoped_observer_.Add(
favicon::ContentFaviconDriver::FromWebContents(contents));
ZoomController::FromWebContents(contents)->AddObserver(this);
int tab_id = ExtensionTabUtil::GetTabId(contents);
DCHECK(tab_entries_.find(tab_id) == tab_entries_.end());
tab_entries_[tab_id] = base::WrapUnique(new TabEntry(this, contents));
}
void TabsEventRouter::UnregisterForTabNotifications(WebContents* contents) {
favicon_scoped_observer_.Remove(
favicon::ContentFaviconDriver::FromWebContents(contents));
ZoomController::FromWebContents(contents)->RemoveObserver(this);
int tab_id = ExtensionTabUtil::GetTabId(contents);
int removed_count = tab_entries_.erase(tab_id);
DCHECK_GT(removed_count, 0);
}
void TabsEventRouter::OnBrowserSetLastActive(Browser* browser) {
TabsWindowsAPI* tabs_window_api = TabsWindowsAPI::Get(profile_);
if (tabs_window_api) {
tabs_window_api->windows_event_router()->OnActiveWindowChanged(
browser ? browser->extension_window_controller() : NULL);
}
}
static bool WillDispatchTabCreatedEvent(
WebContents* contents,
bool active,
content::BrowserContext* context,
const Extension* extension,
Event* event,
const base::DictionaryValue* listener_filter) {
base::DictionaryValue* tab_value =
ExtensionTabUtil::CreateTabObject(contents, extension)
->ToValue()
.release();
event->event_args->Clear();
event->event_args->Append(tab_value);
tab_value->SetBoolean(tabs_constants::kSelectedKey, active);
tab_value->SetBoolean(tabs_constants::kActiveKey, active);
return true;
}
void TabsEventRouter::TabCreatedAt(WebContents* contents,
int index,
bool active) {
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
std::unique_ptr<base::ListValue> args(new base::ListValue);
std::unique_ptr<Event> event(new Event(
events::TABS_ON_CREATED, tabs::OnCreated::kEventName, std::move(args)));
event->restrict_to_browser_context = profile;
event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
event->will_dispatch_callback =
base::Bind(&WillDispatchTabCreatedEvent, contents, active);
EventRouter::Get(profile)->BroadcastEvent(std::move(event));
RegisterForTabNotifications(contents);
}
void TabsEventRouter::TabInsertedAt(WebContents* contents,
int index,
bool active) {
if (!GetTabEntry(contents)) {
// We've never seen this tab, send create event as long as we're not in the
// constructor.
if (browser_tab_strip_tracker_.is_processing_initial_browsers())
RegisterForTabNotifications(contents);
else
TabCreatedAt(contents, index, active);
return;
}
int tab_id = ExtensionTabUtil::GetTabId(contents);
std::unique_ptr<base::ListValue> args(new base::ListValue);
args->AppendInteger(tab_id);
base::DictionaryValue* object_args = new base::DictionaryValue();
object_args->Set(tabs_constants::kNewWindowIdKey,
new FundamentalValue(
ExtensionTabUtil::GetWindowIdOfTab(contents)));
object_args->Set(tabs_constants::kNewPositionKey,
new FundamentalValue(index));
args->Append(object_args);
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
DispatchEvent(profile, events::TABS_ON_ATTACHED, tabs::OnAttached::kEventName,
std::move(args), EventRouter::USER_GESTURE_UNKNOWN);
}
void TabsEventRouter::TabDetachedAt(WebContents* contents, int index) {
if (!GetTabEntry(contents)) {
// The tab was removed. Don't send detach event.
return;
}
std::unique_ptr<base::ListValue> args(new base::ListValue);
args->AppendInteger(ExtensionTabUtil::GetTabId(contents));
base::DictionaryValue* object_args = new base::DictionaryValue();
object_args->Set(tabs_constants::kOldWindowIdKey,
new FundamentalValue(
ExtensionTabUtil::GetWindowIdOfTab(contents)));
object_args->Set(tabs_constants::kOldPositionKey,
new FundamentalValue(index));
args->Append(object_args);
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
DispatchEvent(profile, events::TABS_ON_DETACHED, tabs::OnDetached::kEventName,
std::move(args), EventRouter::USER_GESTURE_UNKNOWN);
}
void TabsEventRouter::TabClosingAt(TabStripModel* tab_strip_model,
WebContents* contents,
int index) {
int tab_id = ExtensionTabUtil::GetTabId(contents);
std::unique_ptr<base::ListValue> args(new base::ListValue);
args->AppendInteger(tab_id);
base::DictionaryValue* object_args = new base::DictionaryValue();
object_args->SetInteger(tabs_constants::kWindowIdKey,
ExtensionTabUtil::GetWindowIdOfTab(contents));
object_args->SetBoolean(tabs_constants::kWindowClosing,
tab_strip_model->closing_all());
args->Append(object_args);
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
DispatchEvent(profile, events::TABS_ON_REMOVED, tabs::OnRemoved::kEventName,
std::move(args), EventRouter::USER_GESTURE_UNKNOWN);
UnregisterForTabNotifications(contents);
}
void TabsEventRouter::ActiveTabChanged(WebContents* old_contents,
WebContents* new_contents,
int index,
int reason) {
std::unique_ptr<base::ListValue> args(new base::ListValue);
int tab_id = ExtensionTabUtil::GetTabId(new_contents);
args->AppendInteger(tab_id);
base::DictionaryValue* object_args = new base::DictionaryValue();
object_args->Set(tabs_constants::kWindowIdKey,
new FundamentalValue(
ExtensionTabUtil::GetWindowIdOfTab(new_contents)));
args->Append(object_args);
// The onActivated event replaced onActiveChanged and onSelectionChanged. The
// deprecated events take two arguments: tabId, {windowId}.
Profile* profile =
Profile::FromBrowserContext(new_contents->GetBrowserContext());
EventRouter::UserGestureState gesture =
reason & CHANGE_REASON_USER_GESTURE
? EventRouter::USER_GESTURE_ENABLED
: EventRouter::USER_GESTURE_NOT_ENABLED;
DispatchEvent(profile, events::TABS_ON_SELECTION_CHANGED,
tabs::OnSelectionChanged::kEventName,
std::unique_ptr<base::ListValue>(args->DeepCopy()), gesture);
DispatchEvent(profile, events::TABS_ON_ACTIVE_CHANGED,
tabs::OnActiveChanged::kEventName,
std::unique_ptr<base::ListValue>(args->DeepCopy()), gesture);
// The onActivated event takes one argument: {windowId, tabId}.
args->Remove(0, NULL);
object_args->Set(tabs_constants::kTabIdKey,
new FundamentalValue(tab_id));
DispatchEvent(profile, events::TABS_ON_ACTIVATED,
tabs::OnActivated::kEventName, std::move(args), gesture);
}
void TabsEventRouter::TabSelectionChanged(
TabStripModel* tab_strip_model,
const ui::ListSelectionModel& old_model) {
ui::ListSelectionModel::SelectedIndices new_selection =
tab_strip_model->selection_model().selected_indices();
std::unique_ptr<base::ListValue> all_tabs(new base::ListValue);
for (size_t i = 0; i < new_selection.size(); ++i) {
int index = new_selection[i];
WebContents* contents = tab_strip_model->GetWebContentsAt(index);
if (!contents)
break;
int tab_id = ExtensionTabUtil::GetTabId(contents);
all_tabs->AppendInteger(tab_id);
}
std::unique_ptr<base::ListValue> args(new base::ListValue);
std::unique_ptr<base::DictionaryValue> select_info(new base::DictionaryValue);
select_info->Set(
tabs_constants::kWindowIdKey,
new FundamentalValue(
ExtensionTabUtil::GetWindowIdOfTabStripModel(tab_strip_model)));
select_info->Set(tabs_constants::kTabIdsKey, all_tabs.release());
args->Append(std::move(select_info));
// The onHighlighted event replaced onHighlightChanged.
Profile* profile = tab_strip_model->profile();
DispatchEvent(profile, events::TABS_ON_HIGHLIGHT_CHANGED,
tabs::OnHighlightChanged::kEventName,
std::unique_ptr<base::ListValue>(args->DeepCopy()),
EventRouter::USER_GESTURE_UNKNOWN);
DispatchEvent(profile, events::TABS_ON_HIGHLIGHTED,
tabs::OnHighlighted::kEventName, std::move(args),
EventRouter::USER_GESTURE_UNKNOWN);
}
void TabsEventRouter::TabMoved(WebContents* contents,
int from_index,
int to_index) {
std::unique_ptr<base::ListValue> args(new base::ListValue);
args->AppendInteger(ExtensionTabUtil::GetTabId(contents));
base::DictionaryValue* object_args = new base::DictionaryValue();
object_args->Set(tabs_constants::kWindowIdKey,
new FundamentalValue(
ExtensionTabUtil::GetWindowIdOfTab(contents)));
object_args->Set(tabs_constants::kFromIndexKey,
new FundamentalValue(from_index));
object_args->Set(tabs_constants::kToIndexKey,
new FundamentalValue(to_index));
args->Append(object_args);
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
DispatchEvent(profile, events::TABS_ON_MOVED, tabs::OnMoved::kEventName,
std::move(args), EventRouter::USER_GESTURE_UNKNOWN);
}
void TabsEventRouter::TabUpdated(TabEntry* entry,
std::set<std::string> changed_property_names) {
bool audible = entry->web_contents()->WasRecentlyAudible();
if (entry->SetAudible(audible)) {
changed_property_names.insert(tabs_constants::kAudibleKey);
}
bool muted = entry->web_contents()->IsAudioMuted();
if (entry->SetMuted(muted)) {
changed_property_names.insert(tabs_constants::kMutedInfoKey);
}
if (!changed_property_names.empty()) {
DispatchTabUpdatedEvent(entry->web_contents(),
std::move(changed_property_names));
}
}
void TabsEventRouter::FaviconUrlUpdated(WebContents* contents) {
content::NavigationEntry* entry = contents->GetController().GetVisibleEntry();
if (!entry || !entry->GetFavicon().valid)
return;
std::set<std::string> changed_property_names;
changed_property_names.insert(tabs_constants::kFaviconUrlKey);
DispatchTabUpdatedEvent(contents, std::move(changed_property_names));
}
void TabsEventRouter::DispatchEvent(
Profile* profile,
events::HistogramValue histogram_value,
const std::string& event_name,
std::unique_ptr<base::ListValue> args,
EventRouter::UserGestureState user_gesture) {
EventRouter* event_router = EventRouter::Get(profile);
if (!profile_->IsSameProfile(profile) || !event_router)
return;
std::unique_ptr<Event> event(
new Event(histogram_value, event_name, std::move(args)));
event->restrict_to_browser_context = profile;
event->user_gesture = user_gesture;
event_router->BroadcastEvent(std::move(event));
}
void TabsEventRouter::DispatchTabUpdatedEvent(
WebContents* contents,
const std::set<std::string> changed_property_names) {
DCHECK(!changed_property_names.empty());
DCHECK(contents);
// The state of the tab (as seen from the extension point of view) has
// changed. Send a notification to the extension.
std::unique_ptr<base::ListValue> args_base(new base::ListValue);
// First arg: The id of the tab that changed.
args_base->AppendInteger(ExtensionTabUtil::GetTabId(contents));
// Second arg: An object containing the changes to the tab state. Filled in
// by WillDispatchTabUpdatedEvent as a copy of changed_properties, if the
// extension has the tabs permission.
// Third arg: An object containing the state of the tab. Filled in by
// WillDispatchTabUpdatedEvent.
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
std::unique_ptr<Event> event(new Event(events::TABS_ON_UPDATED,
tabs::OnUpdated::kEventName,
std::move(args_base)));
event->restrict_to_browser_context = profile;
event->user_gesture = EventRouter::USER_GESTURE_NOT_ENABLED;
event->will_dispatch_callback =
base::Bind(&WillDispatchTabUpdatedEvent, contents,
std::move(changed_property_names));
EventRouter::Get(profile)->BroadcastEvent(std::move(event));
}
TabsEventRouter::TabEntry* TabsEventRouter::GetTabEntry(WebContents* contents) {
const auto it = tab_entries_.find(ExtensionTabUtil::GetTabId(contents));
return it == tab_entries_.end() ? nullptr : it->second.get();
}
void TabsEventRouter::TabChangedAt(WebContents* contents,
int index,
TabChangeType change_type) {
TabEntry* entry = GetTabEntry(contents);
// TabClosingAt() may have already removed the entry for |contents| even
// though the tab has not yet been detached.
if (entry)
TabUpdated(entry, entry->UpdateLoadState());
}
void TabsEventRouter::TabReplacedAt(TabStripModel* tab_strip_model,
WebContents* old_contents,
WebContents* new_contents,
int index) {
// Notify listeners that the next tabs closing or being added are due to
// WebContents being swapped.
const int new_tab_id = ExtensionTabUtil::GetTabId(new_contents);
const int old_tab_id = ExtensionTabUtil::GetTabId(old_contents);
std::unique_ptr<base::ListValue> args(new base::ListValue);
args->AppendInteger(new_tab_id);
args->AppendInteger(old_tab_id);
DispatchEvent(Profile::FromBrowserContext(new_contents->GetBrowserContext()),
events::TABS_ON_REPLACED, tabs::OnReplaced::kEventName,
std::move(args), EventRouter::USER_GESTURE_UNKNOWN);
UnregisterForTabNotifications(old_contents);
if (!GetTabEntry(new_contents))
RegisterForTabNotifications(new_contents);
}
void TabsEventRouter::TabPinnedStateChanged(WebContents* contents, int index) {
TabStripModel* tab_strip = NULL;
int tab_index;
if (ExtensionTabUtil::GetTabStripModel(contents, &tab_strip, &tab_index)) {
std::set<std::string> changed_property_names;
changed_property_names.insert(tabs_constants::kPinnedKey);
DispatchTabUpdatedEvent(contents, std::move(changed_property_names));
}
}
void TabsEventRouter::OnZoomChanged(
const ZoomController::ZoomChangedEventData& data) {
DCHECK(data.web_contents);
int tab_id = ExtensionTabUtil::GetTabId(data.web_contents);
if (tab_id < 0)
return;
// Prepare the zoom change information.
api::tabs::OnZoomChange::ZoomChangeInfo zoom_change_info;
zoom_change_info.tab_id = tab_id;
zoom_change_info.old_zoom_factor =
content::ZoomLevelToZoomFactor(data.old_zoom_level);
zoom_change_info.new_zoom_factor =
content::ZoomLevelToZoomFactor(data.new_zoom_level);
ZoomModeToZoomSettings(data.zoom_mode,
&zoom_change_info.zoom_settings);
// Dispatch the |onZoomChange| event.
Profile* profile = Profile::FromBrowserContext(
data.web_contents->GetBrowserContext());
DispatchEvent(profile, events::TABS_ON_ZOOM_CHANGE,
tabs::OnZoomChange::kEventName,
api::tabs::OnZoomChange::Create(zoom_change_info),
EventRouter::USER_GESTURE_UNKNOWN);
}
void TabsEventRouter::OnFaviconUpdated(
favicon::FaviconDriver* favicon_driver,
NotificationIconType notification_icon_type,
const GURL& icon_url,
bool icon_url_changed,
const gfx::Image& image) {
if (notification_icon_type == NON_TOUCH_16_DIP && icon_url_changed) {
favicon::ContentFaviconDriver* content_favicon_driver =
static_cast<favicon::ContentFaviconDriver*>(favicon_driver);
FaviconUrlUpdated(content_favicon_driver->web_contents());
}
}
} // namespace extensions