| // Copyright 2017 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/resource_coordinator/tab_lifecycle_unit.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/optional.h" |
| #include "base/process/process_metrics.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/devtools/devtools_window.h" |
| #include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h" |
| #include "chrome/browser/media/webrtc/media_stream_capture_indicator.h" |
| #include "chrome/browser/permissions/permission_manager_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/intervention_policy_database.h" |
| #include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom.h" |
| #include "chrome/browser/resource_coordinator/tab_activity_watcher.h" |
| #include "chrome/browser/resource_coordinator/tab_helper.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_observer.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h" |
| #include "chrome/browser/resource_coordinator/tab_load_tracker.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_features.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| #include "chrome/browser/resource_coordinator/utils.h" |
| #include "chrome/browser/tab_contents/form_interaction_tab_helper.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/usb/usb_tab_helper.h" |
| #include "chrome/browser/web_applications/components/web_app_ui_manager.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/performance_manager/public/decorators/page_live_state_decorator.h" |
| #include "components/permissions/permission_manager.h" |
| #include "components/permissions/permission_result.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/blink/public/mojom/frame/sudden_termination_disabler_type.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace resource_coordinator { |
| |
| namespace { |
| |
| using StateChangeReason = LifecycleUnitStateChangeReason; |
| |
| |
| // Returns true if it is valid to transition from |from| to |to| for |reason|. |
| bool IsValidStateChange(LifecycleUnitState from, |
| LifecycleUnitState to, |
| StateChangeReason reason) { |
| switch (from) { |
| case LifecycleUnitState::ACTIVE: { |
| switch (to) { |
| // Discard(URGENT|EXTERNAL) is called. |
| case LifecycleUnitState::DISCARDED: { |
| return reason == StateChangeReason::SYSTEM_MEMORY_PRESSURE || |
| reason == StateChangeReason::EXTENSION_INITIATED; |
| } |
| case LifecycleUnitState::FROZEN: { |
| // Render-initiated freezing, which happens when freezing a page |
| // through ChromeDriver. |
| return reason == StateChangeReason::RENDERER_INITIATED; |
| } |
| default: |
| return false; |
| } |
| } |
| case LifecycleUnitState::THROTTLED: { |
| return false; |
| } |
| case LifecycleUnitState::FROZEN: { |
| switch (to) { |
| // The renderer notifies the browser that the page was unfrozen after |
| // it became visible. |
| case LifecycleUnitState::ACTIVE: { |
| return reason == StateChangeReason::RENDERER_INITIATED; |
| } |
| // Discard(URGENT|EXTERNAL) is called. |
| case LifecycleUnitState::DISCARDED: { |
| return reason == StateChangeReason::BROWSER_INITIATED || |
| reason == StateChangeReason::SYSTEM_MEMORY_PRESSURE || |
| reason == StateChangeReason::EXTENSION_INITIATED; |
| } |
| default: |
| return false; |
| } |
| } |
| case LifecycleUnitState::DISCARDED: { |
| switch (to) { |
| // The WebContents is focused or reloaded. |
| case LifecycleUnitState::ACTIVE: |
| return reason == StateChangeReason::USER_INITIATED; |
| default: |
| return false; |
| } |
| } |
| } |
| } |
| |
| StateChangeReason DiscardReasonToStateChangeReason( |
| LifecycleUnitDiscardReason reason) { |
| switch (reason) { |
| case LifecycleUnitDiscardReason::EXTERNAL: |
| return StateChangeReason::EXTENSION_INITIATED; |
| case LifecycleUnitDiscardReason::URGENT: |
| return StateChangeReason::SYSTEM_MEMORY_PRESSURE; |
| } |
| } |
| |
| } // namespace |
| |
| class TabLifecycleUnitExternalImpl : public TabLifecycleUnitExternal { |
| public: |
| explicit TabLifecycleUnitExternalImpl( |
| TabLifecycleUnitSource::TabLifecycleUnit* tab_lifecycle_unit) |
| : tab_lifecycle_unit_(tab_lifecycle_unit) {} |
| |
| // TabLifecycleUnitExternal: |
| |
| content::WebContents* GetWebContents() const override { |
| return tab_lifecycle_unit_->web_contents(); |
| } |
| |
| bool IsAutoDiscardable() const override { |
| return tab_lifecycle_unit_->IsAutoDiscardable(); |
| } |
| |
| void SetAutoDiscardable(bool auto_discardable) override { |
| tab_lifecycle_unit_->SetAutoDiscardable(auto_discardable); |
| } |
| |
| bool DiscardTab(LifecycleUnitDiscardReason reason) override { |
| return tab_lifecycle_unit_->Discard(reason); |
| } |
| |
| bool IsDiscarded() const override { |
| return tab_lifecycle_unit_->GetState() == LifecycleUnitState::DISCARDED; |
| } |
| |
| int GetDiscardCount() const override { |
| return tab_lifecycle_unit_->GetDiscardCount(); |
| } |
| |
| private: |
| TabLifecycleUnitSource::TabLifecycleUnit* tab_lifecycle_unit_ = nullptr; |
| }; |
| |
| TabLifecycleUnitSource::TabLifecycleUnit::TabLifecycleUnit( |
| TabLifecycleUnitSource* source, |
| base::ObserverList<TabLifecycleObserver>::Unchecked* observers, |
| UsageClock* usage_clock, |
| content::WebContents* web_contents, |
| TabStripModel* tab_strip_model) |
| : LifecycleUnitBase(source, web_contents->GetVisibility(), usage_clock), |
| content::WebContentsObserver(web_contents), |
| observers_(observers), |
| tab_strip_model_(tab_strip_model) { |
| DCHECK(observers_); |
| DCHECK(web_contents); |
| DCHECK(tab_strip_model_); |
| |
| // Attach the ResourceCoordinatorTabHelper. In production code this has |
| // already been attached by now due to AttachTabHelpers, but there's a long |
| // tail of tests that don't add these helpers. This ensures that the various |
| // DCHECKs in the state transition machinery don't fail. |
| ResourceCoordinatorTabHelper::CreateForWebContents(web_contents); |
| |
| // Visible tabs are treated as having been immediately focused, while |
| // non-visible tabs have their focus set to the last active time (the time at |
| // which they stopped being the active tab in a tabstrip). |
| if (GetVisibility() == content::Visibility::VISIBLE) |
| last_focused_time_ = NowTicks(); |
| else |
| last_focused_time_ = web_contents->GetLastActiveTime(); |
| } |
| |
| TabLifecycleUnitSource::TabLifecycleUnit::~TabLifecycleUnit() { |
| OnLifecycleUnitDestroyed(); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::SetTabStripModel( |
| TabStripModel* tab_strip_model) { |
| tab_strip_model_ = tab_strip_model; |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::SetWebContents( |
| content::WebContents* web_contents) { |
| DCHECK(web_contents); |
| Observe(web_contents); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::SetFocused(bool focused) { |
| const bool was_focused = last_focused_time_ == base::TimeTicks::Max(); |
| if (focused == was_focused) |
| return; |
| last_focused_time_ = focused ? base::TimeTicks::Max() : NowTicks(); |
| |
| if (!focused) |
| return; |
| |
| switch (GetState()) { |
| case LifecycleUnitState::DISCARDED: { |
| // Reload the tab. |
| SetState(LifecycleUnitState::ACTIVE, StateChangeReason::USER_INITIATED); |
| bool loaded = Load(); |
| DCHECK(loaded); |
| break; |
| } |
| |
| default: |
| break; |
| } |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::SetRecentlyAudible( |
| bool recently_audible) { |
| if (recently_audible) |
| recently_audible_time_ = base::TimeTicks::Max(); |
| else if (recently_audible_time_ == base::TimeTicks::Max()) |
| recently_audible_time_ = NowTicks(); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::UpdateLifecycleState( |
| performance_manager::mojom::LifecycleState state) { |
| switch (state) { |
| case performance_manager::mojom::LifecycleState::kFrozen: { |
| SetState(LifecycleUnitState::FROZEN, |
| StateChangeReason::RENDERER_INITIATED); |
| break; |
| } |
| |
| case performance_manager::mojom::LifecycleState::kRunning: { |
| SetState(LifecycleUnitState::ACTIVE, |
| StateChangeReason::RENDERER_INITIATED); |
| break; |
| } |
| |
| default: { |
| NOTREACHED(); |
| break; |
| } |
| } |
| } |
| |
| TabLifecycleUnitExternal* |
| TabLifecycleUnitSource::TabLifecycleUnit::AsTabLifecycleUnitExternal() { |
| // Create an impl the first time this is called. |
| if (!external_impl_) |
| external_impl_ = std::make_unique<TabLifecycleUnitExternalImpl>(this); |
| return external_impl_.get(); |
| } |
| |
| base::string16 TabLifecycleUnitSource::TabLifecycleUnit::GetTitle() const { |
| return web_contents()->GetTitle(); |
| } |
| |
| base::TimeTicks TabLifecycleUnitSource::TabLifecycleUnit::GetLastFocusedTime() |
| const { |
| return last_focused_time_; |
| } |
| |
| base::ProcessHandle TabLifecycleUnitSource::TabLifecycleUnit::GetProcessHandle() |
| const { |
| content::RenderFrameHost* main_frame = web_contents()->GetMainFrame(); |
| if (!main_frame) |
| return base::ProcessHandle(); |
| content::RenderProcessHost* process = main_frame->GetProcess(); |
| if (!process) |
| return base::ProcessHandle(); |
| return process->GetProcess().Handle(); |
| } |
| |
| LifecycleUnit::SortKey TabLifecycleUnitSource::TabLifecycleUnit::GetSortKey() |
| const { |
| return SortKey(last_focused_time_); |
| } |
| |
| content::Visibility TabLifecycleUnitSource::TabLifecycleUnit::GetVisibility() |
| const { |
| return web_contents()->GetVisibility(); |
| } |
| |
| LifecycleUnitLoadingState |
| TabLifecycleUnitSource::TabLifecycleUnit::GetLoadingState() const { |
| return TabLoadTracker::Get()->GetLoadingState(web_contents()); |
| } |
| |
| bool TabLifecycleUnitSource::TabLifecycleUnit::Load() { |
| if (GetLoadingState() != LifecycleUnitLoadingState::UNLOADED) |
| return false; |
| |
| // TODO(chrisha): Make this work more elegantly in the case of background tab |
| // loading as well, which uses a NavigationThrottle that can be released. |
| |
| // See comment in Discard() for an explanation of why "needs reload" is |
| // false when a tab is discarded. |
| // TODO(fdoray): Remove NavigationControllerImpl::needs_reload_ once |
| // session restore is handled by LifecycleManager. |
| web_contents()->GetController().SetNeedsReload(); |
| web_contents()->GetController().LoadIfNecessary(); |
| return true; |
| } |
| |
| int TabLifecycleUnitSource::TabLifecycleUnit:: |
| GetEstimatedMemoryFreedOnDiscardKB() const { |
| return GetPrivateMemoryKB(GetProcessHandle()); |
| } |
| |
| bool TabLifecycleUnitSource::TabLifecycleUnit::CanDiscard( |
| LifecycleUnitDiscardReason reason, |
| DecisionDetails* decision_details) const { |
| DCHECK(decision_details->reasons().empty()); |
| |
| // Leave the |decision_details| empty and return immediately for "trivial" |
| // rejection reasons. These aren't worth reporting about, as they have nothing |
| // to do with the content itself. |
| |
| // Can't discard a tab that isn't in a TabStripModel. |
| if (!tab_strip_model_) |
| return false; |
| |
| if (!IsValidStateChange(GetState(), LifecycleUnitState::DISCARDED, |
| DiscardReasonToStateChangeReason(reason))) { |
| return false; |
| } |
| |
| if (web_contents()->IsCrashed()) |
| return false; |
| |
| // Do not discard tabs that don't have a valid URL (most probably they have |
| // just been opened and discarding them would lose the URL). |
| // TODO(fdoray): Look into a workaround to be able to kill the tab without |
| // losing the pending navigation. |
| if (!web_contents()->GetLastCommittedURL().is_valid() || |
| web_contents()->GetLastCommittedURL().is_empty()) { |
| return false; |
| } |
| |
| // Fix for urgent discarding woes in crbug.com/883071. These protections only |
| // apply on non-ChromeOS desktop platforms (Linux, Mac, Win). |
| // NOTE: These do not currently provide DecisionDetails! |
| #if !BUILDFLAG(IS_CHROMEOS_ASH) |
| if (reason == LifecycleUnitDiscardReason::URGENT) { |
| // Limit urgent discarding to once only, unless discarding for the |
| // enterprise memory limit feature. |
| if (GetDiscardCount() > 0 && |
| !GetTabSource()->memory_limit_enterprise_policy()) |
| return false; |
| // Protect non-visible tabs from urgent discarding for a period of time. |
| if (web_contents()->GetVisibility() != content::Visibility::VISIBLE) { |
| base::TimeDelta time_in_bg = NowTicks() - GetWallTimeWhenHidden(); |
| // TODO(sebmarchand): Check if this should be lowered when the enterprise |
| // memory limit feature is set. |
| if (time_in_bg < kBackgroundUrgentProtectionTime) |
| return false; |
| } |
| } |
| #endif |
| |
| // IMPORTANT: Only the first reason added to |decision_details| determines |
| // whether the tab can be discarded. Additional reasons can be added for |
| // reporting purposes, but do not affect whether the tab can be discarded. |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (web_contents()->GetVisibility() == content::Visibility::VISIBLE) |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_VISIBLE); |
| #else |
| // Do not discard the tab if it is currently active in its window. |
| if (tab_strip_model_->GetActiveWebContents() == web_contents()) |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_VISIBLE); |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| // Do not discard tabs in which the user has entered text in a form. |
| |
| // The FormInteractionTabHelper isn't available in some unit tests. |
| if (auto* form_interaction_helper = |
| FormInteractionTabHelper::FromWebContents(web_contents())) { |
| if (form_interaction_helper->had_form_interaction()) |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_FORM_ENTRY); |
| } |
| |
| // Do not discard PDFs as they might contain entry that is not saved and they |
| // don't remember their scrolling positions. See crbug.com/547286 and |
| // crbug.com/65244. |
| // TODO(fdoray): Remove this workaround when the bugs are fixed. |
| if (web_contents()->GetContentsMimeType() == "application/pdf") |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_IS_PDF); |
| |
| // Do not discard a tab that was explicitly disallowed to. |
| if (!auto_discardable_) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_EXTENSION_DISALLOWED); |
| } |
| |
| // Do not discard tabs using media. |
| CheckMediaUsage(decision_details); |
| |
| // Do not discard tabs using device APIs, as this usually breaks |
| // functionality. |
| CheckDeviceUsage(decision_details); |
| |
| // Do not discard tabs that are currently using DevTools, as this can break a |
| // debugging session. |
| if (DevToolsWindow::GetInstanceForInspectedWebContents(web_contents())) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_DEVTOOLS_OPEN); |
| } |
| |
| web_app::WebAppProvider* web_app_provider = |
| web_app::WebAppProvider::GetForWebContents(web_contents()); |
| if (web_app_provider && |
| web_app_provider->ui_manager().IsInAppWindow(web_contents())) { |
| // Do not discard Desktop PWA windows. Preserve native-app experience. |
| decision_details->AddReason(DecisionFailureReason::LIVE_WEB_APP); |
| } |
| |
| if (decision_details->reasons().empty()) { |
| decision_details->AddReason( |
| DecisionSuccessReason::HEURISTIC_OBSERVED_TO_BE_SAFE); |
| DCHECK(decision_details->IsPositive()); |
| } |
| return decision_details->IsPositive(); |
| } |
| |
| ukm::SourceId TabLifecycleUnitSource::TabLifecycleUnit::GetUkmSourceId() const { |
| resource_coordinator::ResourceCoordinatorTabHelper* observer = |
| resource_coordinator::ResourceCoordinatorTabHelper::FromWebContents( |
| web_contents()); |
| if (!observer) |
| return ukm::kInvalidSourceId; |
| return observer->ukm_source_id(); |
| } |
| |
| bool TabLifecycleUnitSource::TabLifecycleUnit::IsAutoDiscardable() const { |
| return auto_discardable_; |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::SetAutoDiscardable( |
| bool auto_discardable) { |
| if (auto_discardable_ == auto_discardable) |
| return; |
| auto_discardable_ = auto_discardable; |
| |
| performance_manager::PageLiveStateDecorator::SetIsAutoDiscardable( |
| web_contents(), auto_discardable_); |
| |
| for (auto& observer : *observers_) |
| observer.OnAutoDiscardableStateChange(web_contents(), auto_discardable_); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::FinishDiscard( |
| LifecycleUnitDiscardReason discard_reason) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "TabManager.Discarding.DiscardedTabHasBeforeUnloadHandler", |
| web_contents()->NeedToFireBeforeUnloadOrUnloadEvents()); |
| |
| content::WebContents* const old_contents = web_contents(); |
| content::WebContents::CreateParams create_params(tab_strip_model_->profile()); |
| // TODO(fdoray): Consider setting |initially_hidden| to true when the tab is |
| // OCCLUDED. Will require checking that the tab reload correctly when it |
| // becomes VISIBLE. |
| create_params.initially_hidden = |
| old_contents->GetVisibility() == content::Visibility::HIDDEN; |
| create_params.desired_renderer_state = |
| content::WebContents::CreateParams::kNoRendererProcess; |
| create_params.last_active_time = old_contents->GetLastActiveTime(); |
| std::unique_ptr<content::WebContents> null_contents = |
| content::WebContents::Create(create_params); |
| content::WebContents* raw_null_contents = null_contents.get(); |
| |
| // Attach the ResourceCoordinatorTabHelper. In production code this has |
| // already been attached by now due to AttachTabHelpers, but there's a long |
| // tail of tests that don't add these helpers. This ensures that the various |
| // DCHECKs in the state transition machinery don't fail. |
| ResourceCoordinatorTabHelper::CreateForWebContents(raw_null_contents); |
| |
| // Copy over the state from the navigation controller to preserve the |
| // back/forward history and to continue to display the correct title/favicon. |
| // |
| // Set |needs_reload| to false so that the tab is not automatically reloaded |
| // when activated. If it was true, there would be an immediate reload when the |
| // active tab of a non-visible window is discarded. SetFocused() will take |
| // care of reloading the tab when it becomes active in a focused window. |
| null_contents->GetController().CopyStateFrom(&old_contents->GetController(), |
| /* needs_reload */ false); |
| |
| // First try to fast-kill the process, if it's just running a single tab. |
| bool fast_shutdown_success = |
| GetRenderProcessHost()->FastShutdownIfPossible(1u, false); |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (!fast_shutdown_success && |
| discard_reason == LifecycleUnitDiscardReason::URGENT) { |
| content::RenderFrameHost* main_frame = old_contents->GetMainFrame(); |
| // We avoid fast shutdown on tabs with beforeunload handlers on the main |
| // frame, as that is often an indication of unsaved user state. |
| DCHECK(main_frame); |
| if (!main_frame->GetSuddenTerminationDisablerState( |
| blink::mojom::SuddenTerminationDisablerType:: |
| kBeforeUnloadHandler)) { |
| fast_shutdown_success = GetRenderProcessHost()->FastShutdownIfPossible( |
| 1u, /* skip_unload_handlers */ true); |
| } |
| UMA_HISTOGRAM_BOOLEAN( |
| "TabManager.Discarding.DiscardedTabCouldUnsafeFastShutdown", |
| fast_shutdown_success); |
| } |
| #endif |
| UMA_HISTOGRAM_BOOLEAN("TabManager.Discarding.DiscardedTabCouldFastShutdown", |
| fast_shutdown_success); |
| |
| // Replace the discarded tab with the null version. |
| const int index = tab_strip_model_->GetIndexOfWebContents(old_contents); |
| DCHECK_NE(index, TabStripModel::kNoTab); |
| |
| // This ensures that on reload after discard, the document has |
| // "WasDiscarded" set to true. |
| // The "WasDiscarded" state is also sent to tab_strip_model. |
| null_contents->SetWasDiscarded(true); |
| |
| std::unique_ptr<content::WebContents> old_contents_deleter = |
| tab_strip_model_->ReplaceWebContentsAt(index, std::move(null_contents)); |
| DCHECK_EQ(web_contents(), raw_null_contents); |
| |
| // Discard the old tab's renderer. |
| // TODO(jamescook): This breaks script connections with other tabs. Find a |
| // different approach that doesn't do that, perhaps based on |
| // RenderFrameProxyHosts. |
| old_contents_deleter.reset(); |
| |
| SetState(LifecycleUnitState::DISCARDED, |
| DiscardReasonToStateChangeReason(discard_reason)); |
| DCHECK_EQ(GetLoadingState(), LifecycleUnitLoadingState::UNLOADED); |
| } |
| |
| bool TabLifecycleUnitSource::TabLifecycleUnit::Discard( |
| LifecycleUnitDiscardReason reason) { |
| // Can't discard a tab when it isn't in a tabstrip. |
| if (!tab_strip_model_) { |
| // Logs are used to diagnose user feedback reports. |
| MEMORY_LOG(ERROR) << "Skipped discarding unit " << GetID() |
| << " because it isn't in a tab strip."; |
| return false; |
| } |
| |
| if (!IsValidStateChange(GetState(), LifecycleUnitState::DISCARDED, |
| DiscardReasonToStateChangeReason(reason))) { |
| // Logs are used to diagnose user feedback reports. |
| MEMORY_LOG(ERROR) << "Skipped discarding unit " << GetID() |
| << " because a transition from " << GetState() |
| << "to discarded is not allowed."; |
| return false; |
| } |
| |
| discard_reason_ = reason; |
| |
| FinishDiscard(reason); |
| |
| return true; |
| } |
| |
| TabLifecycleUnitSource* TabLifecycleUnitSource::TabLifecycleUnit::GetTabSource() |
| const { |
| return static_cast<TabLifecycleUnitSource*>(GetSource()); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::CheckMediaUsage( |
| DecisionDetails* decision_details) const { |
| // TODO(fdoray): Consider being notified of audible, capturing and mirrored |
| // state changes via WebContentsDelegate::NavigationStateChanged() and/or |
| // WebContentsObserver::OnAudioStateChanged and/or RecentlyAudibleHelper. |
| // https://crbug.com/775644 |
| |
| if (recently_audible_time_ == base::TimeTicks::Max() || |
| (!recently_audible_time_.is_null() && |
| NowTicks() - recently_audible_time_ < kTabAudioProtectionTime)) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_PLAYING_AUDIO); |
| } |
| |
| scoped_refptr<MediaStreamCaptureIndicator> media_indicator = |
| MediaCaptureDevicesDispatcher::GetInstance() |
| ->GetMediaStreamCaptureIndicator(); |
| |
| if (media_indicator->IsCapturingUserMedia(web_contents())) { |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_CAPTURING); |
| } |
| |
| if (media_indicator->IsBeingMirrored(web_contents())) { |
| decision_details->AddReason(DecisionFailureReason::LIVE_STATE_MIRRORING); |
| } |
| |
| if (media_indicator->IsCapturingWindow(web_contents()) || |
| media_indicator->IsCapturingDisplay(web_contents())) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_DESKTOP_CAPTURE); |
| } |
| } |
| |
| content::RenderProcessHost* |
| TabLifecycleUnitSource::TabLifecycleUnit::GetRenderProcessHost() const { |
| return web_contents()->GetMainFrame()->GetProcess(); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::OnLifecycleUnitStateChanged( |
| LifecycleUnitState last_state, |
| LifecycleUnitStateChangeReason reason) { |
| DCHECK(IsValidStateChange(last_state, GetState(), reason)) |
| << "Cannot transition TabLifecycleUnit state from " << last_state |
| << " to " << GetState() << " with reason " << reason; |
| |
| // Invoke OnDiscardedStateChange() if necessary. |
| const bool was_discarded = last_state == LifecycleUnitState::DISCARDED; |
| const bool is_discarded = GetState() == LifecycleUnitState::DISCARDED; |
| if (was_discarded != is_discarded) { |
| for (auto& observer : *observers_) { |
| observer.OnDiscardedStateChange(web_contents(), GetDiscardReason(), |
| is_discarded); |
| } |
| } |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::DidStartLoading() { |
| if (GetState() == LifecycleUnitState::DISCARDED) { |
| // This happens when a discarded tab is explicitly reloaded without being |
| // focused first (right-click > Reload). |
| SetState(LifecycleUnitState::ACTIVE, StateChangeReason::USER_INITIATED); |
| } |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::OnVisibilityChanged( |
| content::Visibility visibility) { |
| OnLifecycleUnitVisibilityChanged(visibility); |
| } |
| |
| void TabLifecycleUnitSource::TabLifecycleUnit::CheckDeviceUsage( |
| DecisionDetails* decision_details) const { |
| DCHECK(decision_details); |
| |
| if (auto* usb_tab_helper = UsbTabHelper::FromWebContents(web_contents())) { |
| if (usb_tab_helper->IsDeviceConnected()) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_USING_WEB_USB); |
| } |
| } |
| |
| if (web_contents()->IsConnectedToBluetoothDevice()) { |
| decision_details->AddReason( |
| DecisionFailureReason::LIVE_STATE_USING_BLUETOOTH); |
| } |
| } |
| |
| LifecycleUnitDiscardReason |
| TabLifecycleUnitSource::TabLifecycleUnit::GetDiscardReason() const { |
| return discard_reason_; |
| } |
| |
| } // namespace resource_coordinator |