| // Copyright 2015 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 "components/web_view/frame.h" |
| |
| #include <algorithm> |
| |
| #include "base/auto_reset.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/stl_util.h" |
| #include "components/mus/public/cpp/view.h" |
| #include "components/mus/public/cpp/view_property.h" |
| #include "components/web_view/frame_tree.h" |
| #include "components/web_view/frame_tree_delegate.h" |
| #include "components/web_view/frame_user_data.h" |
| #include "components/web_view/frame_utils.h" |
| #include "mojo/application/public/interfaces/shell.mojom.h" |
| #include "url/gurl.h" |
| |
| using mus::View; |
| |
| DECLARE_VIEW_PROPERTY_TYPE(web_view::Frame*); |
| |
| namespace web_view { |
| |
| // Used to find the Frame associated with a View. |
| DEFINE_LOCAL_VIEW_PROPERTY_KEY(Frame*, kFrame, nullptr); |
| |
| namespace { |
| |
| const uint32_t kNoParentId = 0u; |
| const mus::ConnectionSpecificId kInvalidConnectionId = 0u; |
| |
| mojom::FrameDataPtr FrameToFrameData(const Frame* frame) { |
| mojom::FrameDataPtr frame_data(mojom::FrameData::New()); |
| frame_data->frame_id = frame->id(); |
| frame_data->parent_id = frame->parent() ? frame->parent()->id() : kNoParentId; |
| frame_data->client_properties = |
| mojo::Map<mojo::String, mojo::Array<uint8_t>>::From( |
| frame->client_properties()); |
| return frame_data.Pass(); |
| } |
| |
| } // namespace |
| |
| struct Frame::FrameUserDataAndBinding { |
| scoped_ptr<FrameUserData> user_data; |
| scoped_ptr<mojo::Binding<mojom::Frame>> frame_binding; |
| }; |
| |
| Frame::Frame(FrameTree* tree, |
| View* view, |
| uint32_t frame_id, |
| uint32_t app_id, |
| ViewOwnership view_ownership, |
| mojom::FrameClient* frame_client, |
| scoped_ptr<FrameUserData> user_data, |
| const ClientPropertyMap& client_properties) |
| : tree_(tree), |
| view_(nullptr), |
| embedded_connection_id_(kInvalidConnectionId), |
| id_(frame_id), |
| app_id_(app_id), |
| parent_(nullptr), |
| view_ownership_(view_ownership), |
| user_data_(user_data.Pass()), |
| frame_client_(frame_client), |
| loading_(false), |
| progress_(0.f), |
| client_properties_(client_properties), |
| waiting_for_on_will_navigate_ack_(false), |
| embed_weak_ptr_factory_(this), |
| navigate_weak_ptr_factory_(this) { |
| if (view) |
| SetView(view); |
| } |
| |
| Frame::~Frame() { |
| DVLOG(2) << "~Frame id=" << id_ << " this=" << this; |
| if (view_) |
| view_->RemoveObserver(this); |
| while (!children_.empty()) |
| delete children_[0]; |
| if (parent_) |
| parent_->Remove(this); |
| if (view_) { |
| view_->ClearLocalProperty(kFrame); |
| if (view_ownership_ == ViewOwnership::OWNS_VIEW) |
| view_->Destroy(); |
| } |
| tree_->delegate_->DidDestroyFrame(this); |
| } |
| |
| void Frame::Init(Frame* parent, |
| mojo::ViewTreeClientPtr view_tree_client, |
| mojo::InterfaceRequest<mojom::Frame> frame_request) { |
| { |
| // Set the FrameClient to null so that we don't notify the client of the |
| // add before OnConnect(). |
| base::AutoReset<mojom::FrameClient*> frame_client_resetter(&frame_client_, |
| nullptr); |
| if (parent) |
| parent->Add(this); |
| } |
| |
| const ClientType client_type = frame_request.is_pending() |
| ? ClientType::NEW_CHILD_FRAME |
| : ClientType::EXISTING_FRAME_NEW_APP; |
| InitClient(client_type, nullptr, view_tree_client.Pass(), |
| frame_request.Pass()); |
| |
| tree_->delegate_->DidCreateFrame(this); |
| |
| DVLOG(2) << "Frame id=" << id_ << " parent=" << (parent_ ? parent_->id_ : 0) |
| << " app_id=" << app_id_ << " this=" << this; |
| } |
| |
| // static |
| Frame* Frame::FindFirstFrameAncestor(View* view) { |
| while (view && !view->GetLocalProperty(kFrame)) |
| view = view->parent(); |
| return view ? view->GetLocalProperty(kFrame) : nullptr; |
| } |
| |
| const Frame* Frame::FindFrame(uint32_t id) const { |
| if (id == id_) |
| return this; |
| |
| for (const Frame* child : children_) { |
| const Frame* match = child->FindFrame(id); |
| if (match) |
| return match; |
| } |
| return nullptr; |
| } |
| |
| bool Frame::HasAncestor(const Frame* frame) const { |
| const Frame* current = this; |
| while (current && current != frame) |
| current = current->parent_; |
| return current == frame; |
| } |
| |
| bool Frame::IsLoading() const { |
| if (loading_) |
| return true; |
| for (const Frame* child : children_) { |
| if (child->IsLoading()) |
| return true; |
| } |
| return false; |
| } |
| |
| double Frame::GatherProgress(int* frame_count) const { |
| ++(*frame_count); |
| double progress = progress_; |
| for (const Frame* child : children_) |
| progress += child->GatherProgress(frame_count); |
| return progress_; |
| } |
| |
| void Frame::Find(int32 request_id, |
| const mojo::String& search_text, |
| const FindCallback& callback) { |
| frame_client_->Find(request_id, search_text, callback); |
| } |
| |
| void Frame::StopFinding(bool clear_selection) { |
| frame_client_->StopFinding(clear_selection); |
| } |
| |
| void Frame::HighlightFindResults(int32_t request_id, |
| const mojo::String& search_text, |
| bool reset) { |
| frame_client_->HighlightFindResults(request_id, search_text, reset); |
| } |
| |
| void Frame::StopHighlightingFindResults() { |
| frame_client_->StopHighlightingFindResults(); |
| } |
| |
| void Frame::InitClient(ClientType client_type, |
| scoped_ptr<FrameUserDataAndBinding> data_and_binding, |
| mojo::ViewTreeClientPtr view_tree_client, |
| mojo::InterfaceRequest<mojom::Frame> frame_request) { |
| if (client_type == ClientType::EXISTING_FRAME_NEW_APP && |
| view_tree_client.get()) { |
| embedded_connection_id_ = kInvalidConnectionId; |
| embed_weak_ptr_factory_.InvalidateWeakPtrs(); |
| view_->Embed( |
| view_tree_client.Pass(), mojo::ViewTree::ACCESS_POLICY_DEFAULT, |
| base::Bind(&Frame::OnEmbedAck, embed_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| if (client_type == ClientType::NEW_CHILD_FRAME) { |
| // Don't install an error handler. We allow for the target to only |
| // implement ViewTreeClient. |
| // This frame (and client) was created by an existing FrameClient. There |
| // is no need to send it OnConnect(). |
| frame_binding_.reset( |
| new mojo::Binding<mojom::Frame>(this, frame_request.Pass())); |
| frame_client_->OnConnect( |
| nullptr, tree_->change_id(), id_, mojom::VIEW_CONNECT_TYPE_USE_NEW, |
| mojo::Array<mojom::FrameDataPtr>(), |
| base::Bind(&OnConnectAck, base::Passed(&data_and_binding))); |
| } else { |
| std::vector<const Frame*> frames; |
| tree_->root()->BuildFrameTree(&frames); |
| |
| mojo::Array<mojom::FrameDataPtr> array(frames.size()); |
| for (size_t i = 0; i < frames.size(); ++i) |
| array[i] = FrameToFrameData(frames[i]).Pass(); |
| |
| mojom::FramePtr frame_ptr; |
| // Don't install an error handler. We allow for the target to only |
| // implement ViewTreeClient. |
| frame_binding_.reset( |
| new mojo::Binding<mojom::Frame>(this, GetProxy(&frame_ptr).Pass())); |
| frame_client_->OnConnect( |
| frame_ptr.Pass(), tree_->change_id(), id_, |
| client_type == ClientType::EXISTING_FRAME_SAME_APP |
| ? mojom::VIEW_CONNECT_TYPE_USE_EXISTING |
| : mojom::VIEW_CONNECT_TYPE_USE_NEW, |
| array.Pass(), |
| base::Bind(&OnConnectAck, base::Passed(&data_and_binding))); |
| tree_->delegate_->DidStartNavigation(this); |
| |
| // We need |embedded_connection_id_| is order to validate requests to |
| // create a child frame (OnCreatedFrame()). Pause incoming methods until |
| // we get the id to prevent race conditions. |
| if (embedded_connection_id_ == kInvalidConnectionId) |
| frame_binding_->PauseIncomingMethodCallProcessing(); |
| } |
| } |
| |
| // static |
| void Frame::OnConnectAck(scoped_ptr<FrameUserDataAndBinding> data_and_binding) { |
| } |
| |
| void Frame::ChangeClient(mojom::FrameClient* frame_client, |
| scoped_ptr<FrameUserData> user_data, |
| mojo::ViewTreeClientPtr view_tree_client, |
| uint32_t app_id) { |
| while (!children_.empty()) |
| delete children_[0]; |
| |
| ClientType client_type = view_tree_client.get() == nullptr |
| ? ClientType::EXISTING_FRAME_SAME_APP |
| : ClientType::EXISTING_FRAME_NEW_APP; |
| scoped_ptr<FrameUserDataAndBinding> data_and_binding; |
| |
| if (client_type == ClientType::EXISTING_FRAME_SAME_APP) { |
| // See comment in InitClient() for details. |
| data_and_binding.reset(new FrameUserDataAndBinding); |
| data_and_binding->user_data = user_data_.Pass(); |
| data_and_binding->frame_binding = frame_binding_.Pass(); |
| } else { |
| loading_ = false; |
| progress_ = 0.f; |
| } |
| |
| user_data_ = user_data.Pass(); |
| frame_client_ = frame_client; |
| frame_binding_.reset(); |
| app_id_ = app_id; |
| |
| InitClient(client_type, data_and_binding.Pass(), view_tree_client.Pass(), |
| nullptr); |
| } |
| |
| void Frame::OnEmbedAck(bool success, mus::ConnectionSpecificId connection_id) { |
| if (success) |
| embedded_connection_id_ = connection_id; |
| if (frame_binding_->is_bound()) |
| frame_binding_->ResumeIncomingMethodCallProcessing(); |
| } |
| |
| void Frame::OnWillNavigateAck(mojom::FrameClient* frame_client, |
| scoped_ptr<FrameUserData> user_data, |
| mojo::ViewTreeClientPtr view_tree_client, |
| uint32 app_id) { |
| DCHECK(waiting_for_on_will_navigate_ack_); |
| DVLOG(2) << "Frame::OnWillNavigateAck id=" << id_; |
| waiting_for_on_will_navigate_ack_ = false; |
| ChangeClient(frame_client, user_data.Pass(), view_tree_client.Pass(), app_id); |
| if (pending_navigate_.get()) |
| StartNavigate(pending_navigate_.Pass()); |
| } |
| |
| void Frame::SetView(mus::View* view) { |
| DCHECK(!view_); |
| DCHECK_EQ(id_, view->id()); |
| view_ = view; |
| view_->SetLocalProperty(kFrame, this); |
| view_->AddObserver(this); |
| if (pending_navigate_.get()) |
| StartNavigate(pending_navigate_.Pass()); |
| } |
| |
| void Frame::BuildFrameTree(std::vector<const Frame*>* frames) const { |
| frames->push_back(this); |
| for (const Frame* frame : children_) |
| frame->BuildFrameTree(frames); |
| } |
| |
| void Frame::Add(Frame* node) { |
| DCHECK(!node->parent_); |
| |
| node->parent_ = this; |
| children_.push_back(node); |
| |
| tree_->root()->NotifyAdded(this, node, tree_->AdvanceChangeID()); |
| } |
| |
| void Frame::Remove(Frame* node) { |
| DCHECK_EQ(node->parent_, this); |
| auto iter = std::find(children_.begin(), children_.end(), node); |
| DCHECK(iter != children_.end()); |
| node->parent_ = nullptr; |
| children_.erase(iter); |
| |
| tree_->root()->NotifyRemoved(this, node, tree_->AdvanceChangeID()); |
| } |
| |
| void Frame::StartNavigate(mojo::URLRequestPtr request) { |
| if (waiting_for_on_will_navigate_ack_) { |
| // We're waiting for OnWillNavigate(). We need to wait until that completes |
| // before attempting any other loads. |
| pending_navigate_ = request.Pass(); |
| return; |
| } |
| |
| pending_navigate_.reset(); |
| |
| // We need a View to navigate. When we get the View we'll complete the |
| // navigation. |
| if (!view_) { |
| pending_navigate_ = request.Pass(); |
| return; |
| } |
| |
| // Drop any pending navigation requests. |
| navigate_weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| DVLOG(2) << "Frame::StartNavigate id=" << id_ << " url=" << request->url; |
| |
| const GURL requested_url(request->url); |
| tree_->delegate_->CanNavigateFrame( |
| this, request.Pass(), |
| base::Bind(&Frame::OnCanNavigateFrame, |
| navigate_weak_ptr_factory_.GetWeakPtr(), requested_url)); |
| } |
| |
| void Frame::OnCanNavigateFrame(const GURL& url, |
| uint32_t app_id, |
| mojom::FrameClient* frame_client, |
| scoped_ptr<FrameUserData> user_data, |
| mojo::ViewTreeClientPtr view_tree_client) { |
| DVLOG(2) << "Frame::OnCanNavigateFrame id=" << id_ |
| << " equal=" << (AreAppIdsEqual(app_id, app_id_) ? "true" : "false"); |
| if (AreAppIdsEqual(app_id, app_id_)) { |
| // The app currently rendering the frame will continue rendering it. In this |
| // case we do not use the ViewTreeClient (because the app has a View already |
| // and ends up reusing it). |
| DCHECK(!view_tree_client.get()); |
| ChangeClient(frame_client, user_data.Pass(), view_tree_client.Pass(), |
| app_id); |
| } else { |
| waiting_for_on_will_navigate_ack_ = true; |
| DCHECK(view_tree_client.get()); |
| // TODO(sky): url isn't correct here, it should be a security origin. |
| frame_client_->OnWillNavigate(url.spec(), base::Bind( |
| &Frame::OnWillNavigateAck, base::Unretained(this), frame_client, |
| base::Passed(&user_data), base::Passed(&view_tree_client), app_id)); |
| } |
| } |
| |
| void Frame::NotifyAdded(const Frame* source, |
| const Frame* added_node, |
| uint32_t change_id) { |
| // |frame_client_| may be null during initial frame creation and parenting. |
| if (frame_client_) |
| frame_client_->OnFrameAdded(change_id, FrameToFrameData(added_node)); |
| |
| for (Frame* child : children_) |
| child->NotifyAdded(source, added_node, change_id); |
| } |
| |
| void Frame::NotifyRemoved(const Frame* source, |
| const Frame* removed_node, |
| uint32_t change_id) { |
| frame_client_->OnFrameRemoved(change_id, removed_node->id()); |
| |
| for (Frame* child : children_) |
| child->NotifyRemoved(source, removed_node, change_id); |
| } |
| |
| void Frame::NotifyClientPropertyChanged(const Frame* source, |
| const mojo::String& name, |
| const mojo::Array<uint8_t>& value) { |
| if (this != source) |
| frame_client_->OnFrameClientPropertyChanged(source->id(), name, |
| value.Clone()); |
| |
| for (Frame* child : children_) |
| child->NotifyClientPropertyChanged(source, name, value); |
| } |
| |
| void Frame::NotifyFrameLoadingStateChanged(const Frame* frame, bool loading) { |
| frame_client_->OnFrameLoadingStateChanged(frame->id(), loading); |
| } |
| |
| void Frame::NotifyDispatchFrameLoadEvent(const Frame* frame) { |
| frame_client_->OnDispatchFrameLoadEvent(frame->id()); |
| } |
| |
| void Frame::OnTreeChanged(const TreeChangeParams& params) { |
| if (params.new_parent && this == tree_->root()) { |
| Frame* child_frame = FindFrame(params.target->id()); |
| if (child_frame && !child_frame->view_) |
| child_frame->SetView(params.target); |
| } |
| } |
| |
| void Frame::OnViewDestroying(mus::View* view) { |
| if (parent_) |
| parent_->Remove(this); |
| |
| // Reset |view_ownership_| so we don't attempt to delete |view_| in the |
| // destructor. |
| view_ownership_ = ViewOwnership::DOESNT_OWN_VIEW; |
| |
| if (tree_->root() == this) { |
| view_->RemoveObserver(this); |
| view_ = nullptr; |
| return; |
| } |
| |
| delete this; |
| } |
| |
| void Frame::OnViewEmbeddedAppDisconnected(mus::View* view) { |
| // See FrameTreeDelegate::OnViewEmbeddedAppDisconnected() for details of when |
| // this happens. |
| // |
| // Currently we have no way to distinguish between the cases that lead to this |
| // being called, so we assume we can continue on. Continuing on is important |
| // for html as it's entirely possible for a page to create a frame, navigate |
| // to a bogus url and expect the frame to still exist. |
| tree_->delegate_->OnViewEmbeddedInFrameDisconnected(this); |
| } |
| |
| void Frame::PostMessageEventToFrame(uint32_t target_frame_id, |
| mojom::HTMLMessageEventPtr event) { |
| // NOTE: |target_frame_id| is allowed to be from another connection. |
| Frame* target = tree_->root()->FindFrame(target_frame_id); |
| if (!target || target == this || |
| !tree_->delegate_->CanPostMessageEventToFrame(this, target, event.get())) |
| return; |
| |
| target->frame_client_->OnPostMessageEvent(id_, target_frame_id, event.Pass()); |
| } |
| |
| void Frame::LoadingStateChanged(bool loading, double progress) { |
| bool loading_state_changed = loading_ != loading; |
| loading_ = loading; |
| progress_ = progress; |
| tree_->LoadingStateChanged(); |
| |
| if (loading_state_changed && parent_ && |
| !AreAppIdsEqual(app_id_, parent_->app_id_)) { |
| // We need to notify the parent if it is in a different app, so that it can |
| // keep track of this frame's loading state. If the parent is in the same |
| // app, we assume that the loading state is propagated directly within the |
| // app itself and no notification is needed from our side. |
| parent_->NotifyFrameLoadingStateChanged(this, loading_); |
| } |
| } |
| |
| void Frame::TitleChanged(const mojo::String& title) { |
| // Only care about title changes on the root frame. |
| if (!parent_) |
| tree_->TitleChanged(title); |
| } |
| |
| void Frame::DidCommitProvisionalLoad() { |
| tree_->DidCommitProvisionalLoad(this); |
| } |
| |
| void Frame::SetClientProperty(const mojo::String& name, |
| mojo::Array<uint8_t> value) { |
| auto iter = client_properties_.find(name); |
| const bool already_in_map = (iter != client_properties_.end()); |
| if (value.is_null()) { |
| if (!already_in_map) |
| return; |
| client_properties_.erase(iter); |
| } else { |
| std::vector<uint8_t> as_vector(value.To<std::vector<uint8_t>>()); |
| if (already_in_map && iter->second == as_vector) |
| return; |
| client_properties_[name] = as_vector; |
| } |
| tree_->ClientPropertyChanged(this, name, value); |
| } |
| |
| void Frame::OnCreatedFrame( |
| mojo::InterfaceRequest<mojom::Frame> frame_request, |
| mojom::FrameClientPtr client, |
| uint32_t frame_id, |
| mojo::Map<mojo::String, mojo::Array<uint8_t>> client_properties) { |
| if ((frame_id >> 16) != embedded_connection_id_) { |
| // TODO(sky): kill connection here? |
| DVLOG(1) << "OnCreatedFrame supplied invalid frame id, expecting" |
| << embedded_connection_id_; |
| return; |
| } |
| |
| if (FindFrame(frame_id)) { |
| // TODO(sky): kill connection here? |
| DVLOG(1) << "OnCreatedFrame supplied id of existing frame."; |
| return; |
| } |
| |
| Frame* child_frame = tree_->CreateChildFrame( |
| this, frame_request.Pass(), client.Pass(), frame_id, app_id_, |
| client_properties.To<ClientPropertyMap>()); |
| child_frame->embedded_connection_id_ = embedded_connection_id_; |
| } |
| |
| void Frame::RequestNavigate(mojom::NavigationTargetType target_type, |
| uint32_t target_frame_id, |
| mojo::URLRequestPtr request) { |
| if (target_type == mojom::NAVIGATION_TARGET_TYPE_EXISTING_FRAME) { |
| // |target_frame| is allowed to come from another connection. |
| Frame* target_frame = tree_->root()->FindFrame(target_frame_id); |
| if (!target_frame) { |
| DVLOG(1) << "RequestNavigate EXISTING_FRAME with no matching frame"; |
| return; |
| } |
| if (target_frame != tree_->root()) { |
| target_frame->StartNavigate(request.Pass()); |
| return; |
| } |
| // Else case if |target_frame| == root. Treat at top level request. |
| } |
| tree_->delegate_->NavigateTopLevel(this, request.Pass()); |
| } |
| |
| void Frame::DidNavigateLocally(const mojo::String& url) { |
| NOTIMPLEMENTED(); |
| } |
| |
| void Frame::DispatchLoadEventToParent() { |
| if (parent_ && !AreAppIdsEqual(app_id_, parent_->app_id_)) { |
| // Send notification to fire a load event in the parent, if the parent is in |
| // a different app. If the parent is in the same app, we assume that the app |
| // itself handles firing load event directly and no notification is needed |
| // from our side. |
| parent_->NotifyDispatchFrameLoadEvent(this); |
| } |
| } |
| |
| void Frame::OnFindInFrameCountUpdated(int32_t request_id, |
| int32_t count, |
| bool final_update) { |
| tree_->delegate_->OnFindInFrameCountUpdated(request_id, this, count, |
| final_update); |
| } |
| |
| void Frame::OnFindInPageSelectionUpdated(int32_t request_id, |
| int32_t active_match_ordinal) { |
| tree_->delegate_->OnFindInPageSelectionUpdated(request_id, this, |
| active_match_ordinal); |
| } |
| |
| } // namespace web_view |