| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include <tuple> | 5 #include <tuple> |
| 6 | 6 |
| 7 #include "base/command_line.h" | 7 #include "base/command_line.h" |
| 8 #include "base/location.h" | 8 #include "base/location.h" |
| 9 #include "base/logging.h" | 9 #include "base/logging.h" |
| 10 #include "base/single_thread_task_runner.h" | 10 #include "base/single_thread_task_runner.h" |
| 11 #include "base/threading/thread_task_runner_handle.h" | 11 #include "base/threading/thread_task_runner_handle.h" |
| 12 #include "content/browser/media/session/media_session.h" | 12 #include "content/browser/media/session/media_session_impl.h" |
| 13 #include "content/public/browser/web_contents.h" | 13 #include "content/public/browser/web_contents.h" |
| 14 #include "content/public/common/content_switches.h" | 14 #include "content/public/common/content_switches.h" |
| 15 #include "content/public/test/browser_test_utils.h" | 15 #include "content/public/test/browser_test_utils.h" |
| 16 #include "content/public/test/content_browser_test.h" | 16 #include "content/public/test/content_browser_test.h" |
| 17 #include "content/public/test/content_browser_test_utils.h" | 17 #include "content/public/test/content_browser_test_utils.h" |
| 18 #include "content/public/test/test_navigation_observer.h" | 18 #include "content/public/test/test_navigation_observer.h" |
| 19 #include "content/shell/browser/shell.h" | 19 #include "content/shell/browser/shell.h" |
| 20 #include "media/base/media_switches.h" | 20 #include "media/base/media_switches.h" |
| 21 #include "testing/gtest/include/gtest/gtest.h" | 21 #include "testing/gtest/include/gtest/gtest.h" |
| 22 | 22 |
| (...skipping 24 matching lines...) Expand all Loading... |
| 47 SUSPENDED, | 47 SUSPENDED, |
| 48 INACTIVE, | 48 INACTIVE, |
| 49 }; | 49 }; |
| 50 | 50 |
| 51 struct VisibilityTestData { | 51 struct VisibilityTestData { |
| 52 MediaSuspend media_suspend; | 52 MediaSuspend media_suspend; |
| 53 BackgroundResuming background_resuming; | 53 BackgroundResuming background_resuming; |
| 54 SessionState session_state_before_hide; | 54 SessionState session_state_before_hide; |
| 55 SessionState session_state_after_hide; | 55 SessionState session_state_after_hide; |
| 56 }; | 56 }; |
| 57 | |
| 58 } | 57 } |
| 59 | 58 |
| 60 | |
| 61 // Base class of MediaSession visibility tests. The class is intended | 59 // Base class of MediaSession visibility tests. The class is intended |
| 62 // to be used to run tests under different configurations. Tests | 60 // to be used to run tests under different configurations. Tests |
| 63 // should inheret from this class, set up their own command line per | 61 // should inheret from this class, set up their own command line per |
| 64 // their configuration, and use macro INCLUDE_TEST_FROM_BASE_CLASS to | 62 // their configuration, and use macro INCLUDE_TEST_FROM_BASE_CLASS to |
| 65 // include required tests. See | 63 // include required tests. See |
| 66 // media_session_visibility_browsertest_instances.cc for examples. | 64 // media_session_visibility_browsertest_instances.cc for examples. |
| 67 class MediaSessionVisibilityBrowserTest | 65 class MediaSessionImplVisibilityBrowserTest |
| 68 : public ContentBrowserTest, | 66 : public ContentBrowserTest, |
| 69 public ::testing::WithParamInterface< | 67 public ::testing::WithParamInterface< |
| 70 std::tr1::tuple<VisibilityTestData, Pipeline>> { | 68 std::tr1::tuple<VisibilityTestData, Pipeline>> { |
| 71 public: | 69 public: |
| 72 MediaSessionVisibilityBrowserTest() = default; | 70 MediaSessionImplVisibilityBrowserTest() = default; |
| 73 ~MediaSessionVisibilityBrowserTest() override = default; | 71 ~MediaSessionImplVisibilityBrowserTest() override = default; |
| 74 | 72 |
| 75 void SetUpOnMainThread() override { | 73 void SetUpOnMainThread() override { |
| 76 ContentBrowserTest::SetUpOnMainThread(); | 74 ContentBrowserTest::SetUpOnMainThread(); |
| 77 web_contents_ = shell()->web_contents(); | 75 web_contents_ = shell()->web_contents(); |
| 78 media_session_ = MediaSession::Get(web_contents_); | 76 media_session_ = MediaSessionImpl::Get(web_contents_); |
| 79 | 77 |
| 80 media_session_state_loop_runners_[MediaSession::State::ACTIVE] = | 78 media_session_state_loop_runners_[MediaSessionImpl::State::ACTIVE] = |
| 81 new MessageLoopRunner(); | 79 new MessageLoopRunner(); |
| 82 media_session_state_loop_runners_[MediaSession::State::SUSPENDED] = | 80 media_session_state_loop_runners_[MediaSessionImpl::State::SUSPENDED] = |
| 83 new MessageLoopRunner(); | 81 new MessageLoopRunner(); |
| 84 media_session_state_loop_runners_[MediaSession::State::INACTIVE] = | 82 media_session_state_loop_runners_[MediaSessionImpl::State::INACTIVE] = |
| 85 new MessageLoopRunner(); | 83 new MessageLoopRunner(); |
| 86 media_session_state_callback_subscription_ = | 84 media_session_state_callback_subscription_ = |
| 87 media_session_->RegisterMediaSessionStateChangedCallbackForTest( | 85 media_session_->RegisterMediaSessionStateChangedCallbackForTest( |
| 88 base::Bind(&MediaSessionVisibilityBrowserTest:: | 86 base::Bind(&MediaSessionImplVisibilityBrowserTest:: |
| 89 OnMediaSessionStateChanged, | 87 OnMediaSessionStateChanged, |
| 90 base::Unretained(this))); | 88 base::Unretained(this))); |
| 91 } | 89 } |
| 92 | 90 |
| 93 void TearDownOnMainThread() override { | 91 void TearDownOnMainThread() override { |
| 94 // Unsubscribe the callback subscription before tearing down, so that the | 92 // Unsubscribe the callback subscription before tearing down, so that the |
| 95 // CallbackList in MediaSession will be empty when it is destroyed. | 93 // CallbackList in MediaSession will be empty when it is destroyed. |
| 96 media_session_state_callback_subscription_.reset(); | 94 media_session_state_callback_subscription_.reset(); |
| 97 } | 95 } |
| 98 | 96 |
| 99 void EnableDisableResumingBackgroundVideos(bool enable) { | 97 void EnableDisableResumingBackgroundVideos(bool enable) { |
| 100 std::string enabled_features; | 98 std::string enabled_features; |
| 101 std::string disabled_features; | 99 std::string disabled_features; |
| 102 if (enable) | 100 if (enable) |
| 103 enabled_features = media::kResumeBackgroundVideo.name; | 101 enabled_features = media::kResumeBackgroundVideo.name; |
| 104 else | 102 else |
| 105 disabled_features = media::kResumeBackgroundVideo.name; | 103 disabled_features = media::kResumeBackgroundVideo.name; |
| 106 | 104 |
| 107 std::unique_ptr<base::FeatureList> feature_list(new base::FeatureList); | 105 std::unique_ptr<base::FeatureList> feature_list(new base::FeatureList); |
| 108 feature_list->InitializeFromCommandLine( | 106 feature_list->InitializeFromCommandLine(enabled_features, |
| 109 enabled_features, disabled_features); | 107 disabled_features); |
| 110 base::FeatureList::ClearInstanceForTesting(); | 108 base::FeatureList::ClearInstanceForTesting(); |
| 111 base::FeatureList::SetInstance(std::move(feature_list)); | 109 base::FeatureList::SetInstance(std::move(feature_list)); |
| 112 } | 110 } |
| 113 | 111 |
| 114 void SetUpCommandLine(base::CommandLine* command_line) override { | 112 void SetUpCommandLine(base::CommandLine* command_line) override { |
| 115 command_line->AppendSwitch( | 113 command_line->AppendSwitch( |
| 116 switches::kDisableGestureRequirementForMediaPlayback); | 114 switches::kDisableGestureRequirementForMediaPlayback); |
| 117 #if !defined(OS_ANDROID) | 115 #if !defined(OS_ANDROID) |
| 118 command_line->AppendSwitch( | 116 command_line->AppendSwitch(switches::kEnableDefaultMediaSession); |
| 119 switches::kEnableDefaultMediaSession); | |
| 120 #endif // !defined(OS_ANDROID) | 117 #endif // !defined(OS_ANDROID) |
| 121 | 118 |
| 122 VisibilityTestData params = GetVisibilityTestData(); | 119 VisibilityTestData params = GetVisibilityTestData(); |
| 123 | 120 |
| 124 if (params.media_suspend == MediaSuspend::ENABLED) | 121 if (params.media_suspend == MediaSuspend::ENABLED) |
| 125 command_line->AppendSwitch(switches::kEnableMediaSuspend); | 122 command_line->AppendSwitch(switches::kEnableMediaSuspend); |
| 126 else | 123 else |
| 127 command_line->AppendSwitch(switches::kDisableMediaSuspend); | 124 command_line->AppendSwitch(switches::kDisableMediaSuspend); |
| 128 | 125 |
| 129 #if defined(OS_ANDROID) | 126 #if defined(OS_ANDROID) |
| (...skipping 15 matching lines...) Expand all Loading... |
| 145 return std::tr1::get<0>(GetParam()); | 142 return std::tr1::get<0>(GetParam()); |
| 146 } | 143 } |
| 147 | 144 |
| 148 void StartPlayer() { | 145 void StartPlayer() { |
| 149 LoadTestPage(); | 146 LoadTestPage(); |
| 150 | 147 |
| 151 LOG(INFO) << "Starting player"; | 148 LOG(INFO) << "Starting player"; |
| 152 ClearMediaSessionStateLoopRunners(); | 149 ClearMediaSessionStateLoopRunners(); |
| 153 RunScript(kStartPlayerScript); | 150 RunScript(kStartPlayerScript); |
| 154 LOG(INFO) << "Waiting for session to be active"; | 151 LOG(INFO) << "Waiting for session to be active"; |
| 155 WaitForMediaSessionState(MediaSession::State::ACTIVE); | 152 WaitForMediaSessionState(MediaSessionImpl::State::ACTIVE); |
| 156 } | 153 } |
| 157 | 154 |
| 158 // Maybe pause the player depending on whether the session state before hide | 155 // Maybe pause the player depending on whether the session state before hide |
| 159 // is SUSPENDED. | 156 // is SUSPENDED. |
| 160 void MaybePausePlayer() { | 157 void MaybePausePlayer() { |
| 161 ASSERT_TRUE(GetVisibilityTestData().session_state_before_hide | 158 ASSERT_TRUE(GetVisibilityTestData().session_state_before_hide != |
| 162 != SessionState::INACTIVE); | 159 SessionState::INACTIVE); |
| 163 if (GetVisibilityTestData().session_state_before_hide | 160 if (GetVisibilityTestData().session_state_before_hide == |
| 164 == SessionState::ACTIVE) | 161 SessionState::ACTIVE) |
| 165 return; | 162 return; |
| 166 | 163 |
| 167 LOG(INFO) << "Pausing player"; | 164 LOG(INFO) << "Pausing player"; |
| 168 ClearMediaSessionStateLoopRunners(); | 165 ClearMediaSessionStateLoopRunners(); |
| 169 RunScript(kPausePlayerScript); | 166 RunScript(kPausePlayerScript); |
| 170 LOG(INFO) << "Waiting for session to be suspended"; | 167 LOG(INFO) << "Waiting for session to be suspended"; |
| 171 WaitForMediaSessionState(MediaSession::State::SUSPENDED); | 168 WaitForMediaSessionState(MediaSessionImpl::State::SUSPENDED); |
| 172 } | 169 } |
| 173 | 170 |
| 174 void HideTab() { | 171 void HideTab() { |
| 175 LOG(INFO) << "Hiding the tab"; | 172 LOG(INFO) << "Hiding the tab"; |
| 176 ClearMediaSessionStateLoopRunners(); | 173 ClearMediaSessionStateLoopRunners(); |
| 177 web_contents_->WasHidden(); | 174 web_contents_->WasHidden(); |
| 178 } | 175 } |
| 179 | 176 |
| 180 void CheckSessionStateAfterHide() { | 177 void CheckSessionStateAfterHide() { |
| 181 MediaSession::State state_before_hide = | 178 MediaSessionImpl::State state_before_hide = |
| 182 ToMediaSessionState(GetVisibilityTestData().session_state_before_hide); | 179 ToMediaSessionState(GetVisibilityTestData().session_state_before_hide); |
| 183 MediaSession::State state_after_hide = | 180 MediaSessionImpl::State state_after_hide = |
| 184 ToMediaSessionState(GetVisibilityTestData().session_state_after_hide); | 181 ToMediaSessionState(GetVisibilityTestData().session_state_after_hide); |
| 185 | 182 |
| 186 if (state_before_hide == state_after_hide) { | 183 if (state_before_hide == state_after_hide) { |
| 187 LOG(INFO) << "Waiting for 1 second and check session state is unchanged"; | 184 LOG(INFO) << "Waiting for 1 second and check session state is unchanged"; |
| 188 Wait(base::TimeDelta::FromSeconds(1)); | 185 Wait(base::TimeDelta::FromSeconds(1)); |
| 189 ASSERT_EQ(media_session_->audio_focus_state_, state_after_hide); | 186 ASSERT_EQ(media_session_->audio_focus_state_, state_after_hide); |
| 190 } else { | 187 } else { |
| 191 LOG(INFO) << "Waiting for Session to change"; | 188 LOG(INFO) << "Waiting for Session to change"; |
| 192 WaitForMediaSessionState(state_after_hide); | 189 WaitForMediaSessionState(state_after_hide); |
| 193 } | 190 } |
| (...skipping 10 matching lines...) Expand all Loading... |
| 204 | 201 |
| 205 void RunScript(const std::string& script) { | 202 void RunScript(const std::string& script) { |
| 206 ASSERT_TRUE(ExecuteScript(web_contents_->GetMainFrame(), script)); | 203 ASSERT_TRUE(ExecuteScript(web_contents_->GetMainFrame(), script)); |
| 207 } | 204 } |
| 208 | 205 |
| 209 void ClearMediaSessionStateLoopRunners() { | 206 void ClearMediaSessionStateLoopRunners() { |
| 210 for (auto& state_loop_runner : media_session_state_loop_runners_) | 207 for (auto& state_loop_runner : media_session_state_loop_runners_) |
| 211 state_loop_runner.second = new MessageLoopRunner(); | 208 state_loop_runner.second = new MessageLoopRunner(); |
| 212 } | 209 } |
| 213 | 210 |
| 214 void OnMediaSessionStateChanged(MediaSession::State state) { | 211 void OnMediaSessionStateChanged(MediaSessionImpl::State state) { |
| 215 ASSERT_TRUE(media_session_state_loop_runners_.count(state)); | 212 ASSERT_TRUE(media_session_state_loop_runners_.count(state)); |
| 216 media_session_state_loop_runners_[state]->Quit(); | 213 media_session_state_loop_runners_[state]->Quit(); |
| 217 } | 214 } |
| 218 | 215 |
| 219 // TODO(zqzhang): This method is shared with | 216 // TODO(zqzhang): This method is shared with |
| 220 // MediaRouterIntegrationTests. Move it into a general place. | 217 // MediaRouterIntegrationTests. Move it into a general place. |
| 221 void Wait(base::TimeDelta timeout) { | 218 void Wait(base::TimeDelta timeout) { |
| 222 base::RunLoop run_loop; | 219 base::RunLoop run_loop; |
| 223 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( | 220 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| 224 FROM_HERE, run_loop.QuitClosure(), timeout); | 221 FROM_HERE, run_loop.QuitClosure(), timeout); |
| 225 run_loop.Run(); | 222 run_loop.Run(); |
| 226 } | 223 } |
| 227 | 224 |
| 228 void WaitForMediaSessionState(MediaSession::State state) { | 225 void WaitForMediaSessionState(MediaSessionImpl::State state) { |
| 229 ASSERT_TRUE(media_session_state_loop_runners_.count(state)); | 226 ASSERT_TRUE(media_session_state_loop_runners_.count(state)); |
| 230 media_session_state_loop_runners_[state]->Run(); | 227 media_session_state_loop_runners_[state]->Run(); |
| 231 } | 228 } |
| 232 | 229 |
| 233 MediaSession::State ToMediaSessionState(SessionState state) { | 230 MediaSessionImpl::State ToMediaSessionState(SessionState state) { |
| 234 switch (state) { | 231 switch (state) { |
| 235 case SessionState::ACTIVE: | 232 case SessionState::ACTIVE: |
| 236 return MediaSession::State::ACTIVE; | 233 return MediaSessionImpl::State::ACTIVE; |
| 237 break; | 234 break; |
| 238 case SessionState::SUSPENDED: | 235 case SessionState::SUSPENDED: |
| 239 return MediaSession::State::SUSPENDED; | 236 return MediaSessionImpl::State::SUSPENDED; |
| 240 break; | 237 break; |
| 241 case SessionState::INACTIVE: | 238 case SessionState::INACTIVE: |
| 242 return MediaSession::State::INACTIVE; | 239 return MediaSessionImpl::State::INACTIVE; |
| 243 break; | 240 break; |
| 244 default: | 241 default: |
| 245 ADD_FAILURE() << "invalid SessionState to convert"; | 242 ADD_FAILURE() << "invalid SessionState to convert"; |
| 246 return MediaSession::State::INACTIVE; | 243 return MediaSessionImpl::State::INACTIVE; |
| 247 } | 244 } |
| 248 } | 245 } |
| 249 | 246 |
| 250 WebContents* web_contents_; | 247 WebContents* web_contents_; |
| 251 MediaSession* media_session_; | 248 MediaSessionImpl* media_session_; |
| 252 // MessageLoopRunners for waiting MediaSession state to change. Note that the | 249 // MessageLoopRunners for waiting MediaSession state to change. Note that the |
| 253 // MessageLoopRunners can accept Quit() before calling Run(), thus the state | 250 // MessageLoopRunners can accept Quit() before calling Run(), thus the state |
| 254 // change can still be captured before waiting. For example, the MediaSession | 251 // change can still be captured before waiting. For example, the MediaSession |
| 255 // might go active immediately after calling HTMLMediaElement.play(). A test | 252 // might go active immediately after calling HTMLMediaElement.play(). A test |
| 256 // can listen to the state change before calling play(), and then wait for the | 253 // can listen to the state change before calling play(), and then wait for the |
| 257 // state change after play(). | 254 // state change after play(). |
| 258 std::map<MediaSession::State, scoped_refptr<MessageLoopRunner> > | 255 std::map<MediaSessionImpl::State, scoped_refptr<MessageLoopRunner>> |
| 259 media_session_state_loop_runners_; | 256 media_session_state_loop_runners_; |
| 260 std::unique_ptr<base::CallbackList<void(MediaSession::State)>::Subscription> | 257 std::unique_ptr< |
| 258 base::CallbackList<void(MediaSessionImpl::State)>::Subscription> |
| 261 media_session_state_callback_subscription_; | 259 media_session_state_callback_subscription_; |
| 262 | 260 |
| 263 DISALLOW_COPY_AND_ASSIGN(MediaSessionVisibilityBrowserTest); | 261 DISALLOW_COPY_AND_ASSIGN(MediaSessionImplVisibilityBrowserTest); |
| 264 }; | 262 }; |
| 265 | 263 |
| 266 namespace { | 264 namespace { |
| 267 | 265 |
| 268 VisibilityTestData kTestParams[] = { | 266 VisibilityTestData kTestParams[] = { |
| 269 { MediaSuspend::ENABLED, BackgroundResuming::DISABLED, | 267 {MediaSuspend::ENABLED, BackgroundResuming::DISABLED, |
| 270 SessionState::SUSPENDED, SessionState::INACTIVE }, | 268 SessionState::SUSPENDED, SessionState::INACTIVE}, |
| 271 { MediaSuspend::ENABLED, BackgroundResuming::DISABLED, | 269 {MediaSuspend::ENABLED, BackgroundResuming::DISABLED, SessionState::ACTIVE, |
| 272 SessionState::ACTIVE, SessionState::INACTIVE }, | 270 SessionState::INACTIVE}, |
| 273 { MediaSuspend::ENABLED, BackgroundResuming::ENABLED, | 271 {MediaSuspend::ENABLED, BackgroundResuming::ENABLED, SessionState::ACTIVE, |
| 274 SessionState::ACTIVE, SessionState::SUSPENDED }, | 272 SessionState::SUSPENDED}, |
| 275 { MediaSuspend::ENABLED, BackgroundResuming::ENABLED, | 273 {MediaSuspend::ENABLED, BackgroundResuming::ENABLED, |
| 276 SessionState::SUSPENDED, SessionState::SUSPENDED }, | 274 SessionState::SUSPENDED, SessionState::SUSPENDED}, |
| 277 { MediaSuspend::DISABLED, BackgroundResuming::DISABLED, | 275 {MediaSuspend::DISABLED, BackgroundResuming::DISABLED, |
| 278 SessionState::SUSPENDED, SessionState::SUSPENDED }, | 276 SessionState::SUSPENDED, SessionState::SUSPENDED}, |
| 279 { MediaSuspend::DISABLED, BackgroundResuming::DISABLED, | 277 {MediaSuspend::DISABLED, BackgroundResuming::DISABLED, SessionState::ACTIVE, |
| 280 SessionState::ACTIVE, SessionState::ACTIVE }, | 278 SessionState::ACTIVE}, |
| 281 { MediaSuspend::DISABLED, BackgroundResuming::ENABLED, | 279 {MediaSuspend::DISABLED, BackgroundResuming::ENABLED, SessionState::ACTIVE, |
| 282 SessionState::ACTIVE, SessionState::ACTIVE }, | 280 SessionState::ACTIVE}, |
| 283 { MediaSuspend::DISABLED, BackgroundResuming::ENABLED, | 281 {MediaSuspend::DISABLED, BackgroundResuming::ENABLED, |
| 284 SessionState::SUSPENDED, SessionState::SUSPENDED }, | 282 SessionState::SUSPENDED, SessionState::SUSPENDED}, |
| 285 }; | 283 }; |
| 286 | 284 |
| 287 Pipeline kPipelines[] = { | 285 Pipeline kPipelines[] = { |
| 288 Pipeline::WMPI, | 286 Pipeline::WMPI, |
| 289 #if defined(OS_ANDROID) | 287 #if defined(OS_ANDROID) |
| 290 // Disabling WMPA tests because of https://crbug.com/646312 | 288 // Disabling WMPA tests because of https://crbug.com/646312 |
| 291 // Pipeline::WMPA, | 289 // Pipeline::WMPA, |
| 292 #endif // defined(OS_ANDROID) | 290 #endif // defined(OS_ANDROID) |
| 293 }; | 291 }; |
| 294 | 292 |
| 295 } // anonymous namespace | 293 } // anonymous namespace |
| 296 | 294 |
| 297 IN_PROC_BROWSER_TEST_P(MediaSessionVisibilityBrowserTest, | 295 IN_PROC_BROWSER_TEST_P(MediaSessionImplVisibilityBrowserTest, TestEntryPoint) { |
| 298 TestEntryPoint) { | |
| 299 StartPlayer(); | 296 StartPlayer(); |
| 300 MaybePausePlayer(); | 297 MaybePausePlayer(); |
| 301 HideTab(); | 298 HideTab(); |
| 302 CheckSessionStateAfterHide(); | 299 CheckSessionStateAfterHide(); |
| 303 } | 300 } |
| 304 | 301 |
| 305 INSTANTIATE_TEST_CASE_P(MediaSessionVisibilityBrowserTestInstances, | 302 INSTANTIATE_TEST_CASE_P(MediaSessionImplVisibilityBrowserTestInstances, |
| 306 MediaSessionVisibilityBrowserTest, | 303 MediaSessionImplVisibilityBrowserTest, |
| 307 ::testing::Combine(::testing::ValuesIn(kTestParams), | 304 ::testing::Combine(::testing::ValuesIn(kTestParams), |
| 308 ::testing::ValuesIn(kPipelines))); | 305 ::testing::ValuesIn(kPipelines))); |
| 309 | 306 |
| 310 } // namespace content | 307 } // namespace content |
| OLD | NEW |