Commit 3ac0c0fb authored by Jeffrey Kardatzke's avatar Jeffrey Kardatzke Committed by Commit Bot

Implemented Chrome end of ARC screen capture

This fills out screen capture by implementing the permissions prompt and
the session logic which performs the desktop capture with the GPU and
copies the result to the GPU buffer passed from Android.

It also adds a 'system_modal' parameter to the desktop picker dialog so
that it will display properly over the Android windows.

Design doc is here: goto/arcppscreencapture

Bug: b:38452042
Change-Id: Ia2541a9494485704aa290e77aef1dca4578853e7
Reviewed-on: https://chromium-review.googlesource.com/887642
Commit-Queue: Jeffrey Kardatzke <jkardatzke@google.com>
Reviewed-by: default avatarSergey Ulanov <sergeyu@chromium.org>
Reviewed-by: default avatarQiang Chen <qiangchen@chromium.org>
Reviewed-by: default avatarLuis Hector Chavez <lhchavez@chromium.org>
Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Reviewed-by: default avatarDavid Reveman <reveman@chromium.org>
Reviewed-by: default avatarZijie He <zijiehe@chromium.org>
Reviewed-by: default avatarYusuke Sato <yusukes@chromium.org>
Cr-Commit-Position: refs/heads/master@{#533162}
parent d54774c5
......@@ -664,6 +664,7 @@ split_static_library("browser") {
"media/webrtc/desktop_media_list.h",
"media/webrtc/desktop_media_list_base.cc",
"media/webrtc/desktop_media_list_base.h",
"media/webrtc/desktop_media_picker.cc",
"media/webrtc/desktop_media_picker.h",
"media/webrtc/desktop_streams_registry.cc",
"media/webrtc/desktop_streams_registry.h",
......
......@@ -426,6 +426,8 @@ source_set("chromeos") {
"arc/process/arc_process_service.h",
"arc/screen_capture/arc_screen_capture_bridge.cc",
"arc/screen_capture/arc_screen_capture_bridge.h",
"arc/screen_capture/arc_screen_capture_session.cc",
"arc/screen_capture/arc_screen_capture_session.h",
"arc/tracing/arc_tracing_bridge.cc",
"arc/tracing/arc_tracing_bridge.h",
"arc/tts/arc_tts_service.cc",
......
......@@ -4,9 +4,15 @@
#include "chrome/browser/chromeos/arc/screen_capture/arc_screen_capture_bridge.h"
#include "ash/shell.h"
#include "base/bind.h"
#include "base/memory/singleton.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chromeos/arc/screen_capture/arc_screen_capture_session.h"
#include "chrome/browser/media/webrtc/desktop_media_list_ash.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "content/public/browser/browser_thread.h"
namespace arc {
namespace {
......@@ -52,7 +58,41 @@ void ArcScreenCaptureBridge::RequestPermission(
const std::string& display_name,
const std::string& package_name,
RequestPermissionCallback callback) {
NOTREACHED();
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::unique_ptr<DesktopMediaPicker> picker = DesktopMediaPicker::Create();
std::vector<std::unique_ptr<DesktopMediaList>> source_lists;
source_lists.emplace_back(std::make_unique<DesktopMediaListAsh>(
content::DesktopMediaID::TYPE_SCREEN));
const base::string16 display_name16 = base::UTF8ToUTF16(display_name);
DesktopMediaPicker::Params picker_params;
picker_params.context = ash::Shell::GetRootWindowForDisplayId(
display::Screen::GetScreen()->GetPrimaryDisplay().id());
picker_params.modality = ui::ModalType::MODAL_TYPE_SYSTEM;
picker_params.app_name = display_name16;
picker_params.target_name = display_name16;
picker->Show(
picker_params, std::move(source_lists),
base::BindRepeating(&ArcScreenCaptureBridge::PermissionPromptCallback,
base::Unretained(this), base::Passed(&picker),
display_name, package_name, base::Passed(&callback)));
}
void ArcScreenCaptureBridge::PermissionPromptCallback(
std::unique_ptr<DesktopMediaPicker> picker,
const std::string& display_name,
const std::string& package_name,
RequestPermissionCallback callback,
content::DesktopMediaID desktop_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (desktop_id.is_null()) {
std::move(callback).Run(false);
return;
}
// This may overwrite an existing entry which is OK since these persist
// forever and this may be requested again with a different desktop.
permissions_map_[package_name] =
std::make_unique<GrantedCaptureParams>(display_name, desktop_id);
std::move(callback).Run(true);
}
void ArcScreenCaptureBridge::OpenSession(
......@@ -60,7 +100,19 @@ void ArcScreenCaptureBridge::OpenSession(
const std::string& package_name,
const gfx::Size& size,
OpenSessionCallback callback) {
NOTREACHED();
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto found = permissions_map_.find(package_name);
if (found == permissions_map_.end()) {
LOG(ERROR) << "Attempt to open screen capture session without granted "
"permissions for package "
<< package_name;
std::move(callback).Run(nullptr);
return;
}
DCHECK(found->second) << package_name;
std::move(callback).Run(ArcScreenCaptureSession::Create(
std::move(notifier), found->second->display_name,
found->second->desktop_id, size));
}
} // namespace arc
......@@ -5,9 +5,14 @@
#ifndef CHROME_BROWSER_CHROMEOS_ARC_SCREEN_CAPTURE_ARC_SCREEN_CAPTURE_BRIDGE_H_
#define CHROME_BROWSER_CHROMEOS_ARC_SCREEN_CAPTURE_ARC_SCREEN_CAPTURE_BRIDGE_H_
#include <memory>
#include <unordered_map>
#include "base/macros.h"
#include "chrome/browser/media/webrtc/desktop_media_picker.h"
#include "components/arc/common/screen_capture.mojom.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/desktop_media_id.h"
namespace content {
class BrowserContext;
......@@ -39,8 +44,28 @@ class ArcScreenCaptureBridge : public KeyedService,
RequestPermissionCallback callback) override;
private:
struct GrantedCaptureParams {
GrantedCaptureParams(const std::string display_name,
content::DesktopMediaID desktop_id)
: display_name(display_name), desktop_id(desktop_id) {}
std::string display_name;
content::DesktopMediaID desktop_id;
};
void PermissionPromptCallback(std::unique_ptr<DesktopMediaPicker> picker,
const std::string& display_name,
const std::string& package_name,
RequestPermissionCallback callback,
content::DesktopMediaID desktop_id);
ArcBridgeService* const arc_bridge_service_; // Owned by ArcServiceManager.
// The string in this map corresponds to the passed in package_name when
// RequestPermission is called. That same string should then be passed into
// OpenSession as a token that correlates the two calls. This map is used to
// validate that.
std::unordered_map<std::string, std::unique_ptr<GrantedCaptureParams>>
permissions_map_;
// WeakPtrFactory to use for callbacks.
base::WeakPtrFactory<ArcScreenCaptureBridge> weak_factory_;
......
// Copyright 2018 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.
#ifndef CHROME_BROWSER_CHROMEOS_ARC_SCREEN_CAPTURE_ARC_SCREEN_CAPTURE_SESSION_H_
#define CHROME_BROWSER_CHROMEOS_ARC_SCREEN_CAPTURE_ARC_SCREEN_CAPTURE_SESSION_H_
#include <memory>
#include <queue>
#include <string>
#include "base/macros.h"
#include "components/arc/common/screen_capture.mojom.h"
#include "components/viz/common/gl_helper.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "ui/compositor/compositor_animation_observer.h"
namespace aura {
class Window;
} // namespace aura
namespace content {
struct DesktopMediaID;
class MediaStreamUI;
} // namespace content
namespace gfx {
class GpuMemoryBuffer;
class Size;
} // namespace gfx
namespace viz {
class CopyOutputResult;
} // namespace viz
namespace arc {
class ArcScreenCaptureSession : public mojom::ScreenCaptureSession,
public ui::CompositorAnimationObserver {
public:
// Creates a new ScreenCaptureSession and returns the interface pointer for
// passing back across a Mojo pipe. This object will be automatically
// destructed when the Mojo connection is closed.
static mojom::ScreenCaptureSessionPtr Create(
mojom::ScreenCaptureSessionNotifierPtr notifier,
const std::string& display_name,
content::DesktopMediaID desktop_id,
const gfx::Size& size);
// Implements mojo::ScreenCaptureSession interface.
void SetOutputBuffer(mojo::ScopedHandle graphics_buffer,
uint32_t stride,
SetOutputBufferCallback callback) override;
// Implements ui::CompositorAnimationObserver.
void OnAnimationStep(base::TimeTicks timestamp) override;
void OnCompositingShuttingDown(ui::Compositor* compositor) override;
private:
struct DesktopTexture;
struct PendingBuffer;
ArcScreenCaptureSession(mojom::ScreenCaptureSessionNotifierPtr notifier,
const gfx::Size& size);
~ArcScreenCaptureSession() override;
// Does additional checks and upon success returns a valid InterfacePtr, null
// otherwise.
mojom::ScreenCaptureSessionPtr Initialize(content::DesktopMediaID desktop_id,
const std::string& display_name);
// Copies the GL texture from a desktop capture to the corresponding GL
// texture for a GPU buffer.
void CopyDesktopTextureToGpuBuffer(
std::unique_ptr<DesktopTexture> desktop_texture,
std::unique_ptr<PendingBuffer> pending_buffer);
// Closes the Mojo connection by destroying this object.
void Close();
// Callback for when we perform CopyOutputRequests.
void OnDesktopCaptured(std::unique_ptr<viz::CopyOutputResult> result);
// Callback for completion of GL commands.
void QueryCompleted(std::unique_ptr<gfx::GpuMemoryBuffer> gpu_memory_buffer,
GLuint query_id,
GLuint texture,
GLuint id,
SetOutputBufferCallback callback);
// Callback for a user clicking Stop on the notification for screen capture.
void NotificationStop();
mojo::Binding<mojom::ScreenCaptureSession> binding_;
mojom::ScreenCaptureSessionNotifierPtr notifier_;
gfx::Size size_;
aura::Window* desktop_window_;
// We have 2 separate queues for handling incoming GPU buffers from Android
// and also textures for the desktop we have captured already. Due to the
// parallel nature of the operations, we can end up with starvation in a queue
// if we only implemented a queue for one end of this. This technique allows
// us to maximize throughput and never have overhead with frame duplication as
// well as never skip any output buffers.
std::queue<std::unique_ptr<PendingBuffer>> buffer_queue_;
std::queue<std::unique_ptr<DesktopTexture>> texture_queue_;
std::unique_ptr<viz::GLHelper> gl_helper_;
std::unique_ptr<viz::GLHelper::ScalerInterface> scaler_;
std::unique_ptr<content::MediaStreamUI> notification_ui_;
base::WeakPtrFactory<ArcScreenCaptureSession> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(ArcScreenCaptureSession);
};
} // namespace arc
#endif // CHROME_BROWSER_CHROMEOS_ARC_SCREEN_CAPTURE_ARC_SCREEN_CAPTURE_SESSION_H_
......@@ -66,13 +66,8 @@ class FakeDesktopMediaPicker : public DesktopMediaPicker {
~FakeDesktopMediaPicker() override { expectation_->picker_deleted = true; }
// DesktopMediaPicker interface.
void Show(content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
void Show(const DesktopMediaPicker::Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) override {
bool show_screens = false;
bool show_windows = false;
......@@ -96,7 +91,8 @@ class FakeDesktopMediaPicker : public DesktopMediaPicker {
EXPECT_EQ(expectation_->expect_screens, show_screens);
EXPECT_EQ(expectation_->expect_windows, show_windows);
EXPECT_EQ(expectation_->expect_tabs, show_tabs);
EXPECT_EQ(expectation_->expect_audio, request_audio);
EXPECT_EQ(expectation_->expect_audio, params.request_audio);
EXPECT_EQ(params.modality, ui::ModalType::MODAL_TYPE_CHILD);
if (!expectation_->cancelled) {
// Post a task to call the callback asynchronously.
......
......@@ -204,10 +204,14 @@ bool DesktopCaptureChooseDesktopMediaFunctionBase::Execute(
DesktopMediaPicker::DoneCallback callback = base::Bind(
&DesktopCaptureChooseDesktopMediaFunctionBase::OnPickerDialogResults,
this);
picker_->Show(web_contents, parent_window, parent_window,
base::UTF8ToUTF16(GetCallerDisplayName()), target_name,
std::move(source_lists), request_audio, callback);
DesktopMediaPicker::Params picker_params;
picker_params.web_contents = web_contents;
picker_params.context = parent_window;
picker_params.parent = parent_window;
picker_params.app_name = base::UTF8ToUTF16(GetCallerDisplayName());
picker_params.target_name = target_name;
picker_params.request_audio = request_audio;
picker_->Show(picker_params, std::move(source_lists), callback);
origin_ = origin;
return true;
}
......
// Copyright 2018 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 "desktop_media_picker.h"
DesktopMediaPicker::Params::Params() = default;
DesktopMediaPicker::Params::~Params() = default;
......@@ -11,6 +11,7 @@
#include "base/macros.h"
#include "base/strings/string16.h"
#include "content/public/browser/desktop_media_id.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/native_widget_types.h"
class DesktopMediaList;
......@@ -20,10 +21,33 @@ class WebContents;
}
// Abstract interface for desktop media picker UI. It's used by Desktop Media
// API to let user choose a desktop media source.
// API and by ARC to let user choose a desktop media source.
class DesktopMediaPicker {
public:
typedef base::Callback<void(content::DesktopMediaID)> DoneCallback;
struct Params {
Params();
~Params();
// WebContents this picker is relative to, can be null.
content::WebContents* web_contents = nullptr;
// The context whose root window is used for dialog placement, cannot be
// null for Aura.
gfx::NativeWindow context = nullptr;
// Parent window the dialog is relative to, only used on Mac.
gfx::NativeWindow parent = nullptr;
// The modality used for showing the dialog.
ui::ModalType modality = ui::ModalType::MODAL_TYPE_CHILD;
// The name used in the dialog for what is requesting the picker to be
// shown.
base::string16 app_name;
// Can be the same as target_name. If it is not then this is used in the
// dialog for what is specific target within the app_name is requesting the
// picker.
base::string16 target_name;
// Whether audio capture should be shown as an option in the picker.
bool request_audio = false;
};
// Creates default implementation of DesktopMediaPicker for the current
// platform.
......@@ -33,16 +57,11 @@ class DesktopMediaPicker {
virtual ~DesktopMediaPicker() {}
// Shows dialog with list of desktop media sources (screens, windows, tabs)
// provided by |screen_list|, |window_list| and |tab_list|.
// provided by |sources_lists|.
// Dialog window will call |done_callback| when user chooses one of the
// sources or closes the dialog.
virtual void Show(content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
virtual void Show(const Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) = 0;
private:
......
......@@ -19,13 +19,8 @@ class DesktopMediaPickerCocoa : public DesktopMediaPicker {
~DesktopMediaPickerCocoa() override;
// Overridden from DesktopMediaPicker:
void Show(content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
void Show(const DesktopMediaPicker::Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) override;
private:
......
......@@ -15,21 +15,16 @@ DesktopMediaPickerCocoa::~DesktopMediaPickerCocoa() {
}
void DesktopMediaPickerCocoa::Show(
content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
const DesktopMediaPicker::Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) {
controller_.reset([[DesktopMediaPickerController alloc]
initWithSourceLists:std::move(source_lists)
parent:parent
parent:params.parent
callback:done_callback
appName:app_name
targetName:target_name
requestAudio:request_audio]);
appName:params.app_name
targetName:params.target_name
requestAudio:params.request_audio]);
[controller_ showWindow:nil];
}
......
......@@ -51,14 +51,11 @@ DesktopMediaID::Id AcceleratedWidgetToDesktopMediaId(
} // namespace
DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
content::WebContents* parent_web_contents,
gfx::NativeWindow context,
const DesktopMediaPicker::Params& params,
DesktopMediaPickerViews* parent,
const base::string16& app_name,
const base::string16& target_name,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio)
std::vector<std::unique_ptr<DesktopMediaList>> source_lists)
: parent_(parent),
modality_(params.modality),
description_label_(new views::Label()),
audio_share_checkbox_(nullptr),
pane_(new views::TabbedPane()) {
......@@ -182,19 +179,20 @@ DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
}
}
if (app_name == target_name) {
description_label_->SetText(
l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT, app_name));
} else {
if (params.app_name == params.target_name) {
description_label_->SetText(l10n_util::GetStringFUTF16(
IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED, app_name, target_name));
IDS_DESKTOP_MEDIA_PICKER_TEXT, params.app_name));
} else {
description_label_->SetText(
l10n_util::GetStringFUTF16(IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED,
params.app_name, params.target_name));
}
DCHECK(!source_types_.empty());
pane_->SetFocusBehavior(views::View::FocusBehavior::NEVER);
AddChildView(pane_);
if (request_audio) {
if (params.request_audio) {
audio_share_checkbox_ = new views::Checkbox(
l10n_util::GetStringUTF16(IDS_DESKTOP_MEDIA_PICKER_AUDIO_SHARE));
audio_share_checkbox_->SetChecked(true);
......@@ -203,18 +201,18 @@ DesktopMediaPickerDialogView::DesktopMediaPickerDialogView(
// Focus on the first non-null media_list.
OnSourceTypeSwitched(0);
// If |parent_web_contents| is set and it's not a background page then the
// If |params.web_contents| is set and it's not a background page then the
// picker will be shown modal to the web contents. Otherwise the picker is
// shown in a separate window.
views::Widget* widget = nullptr;
bool modal_dialog =
parent_web_contents &&
!parent_web_contents->GetDelegate()->IsNeverVisible(parent_web_contents);
params.web_contents &&
!params.web_contents->GetDelegate()->IsNeverVisible(params.web_contents);
if (modal_dialog) {
widget =
constrained_window::ShowWebModalDialogViews(this, parent_web_contents);
constrained_window::ShowWebModalDialogViews(this, params.web_contents);
} else {
widget = DialogDelegate::CreateDialogWidget(this, context, nullptr);
widget = DialogDelegate::CreateDialogWidget(this, params.context, nullptr);
widget->Show();
}
chrome::RecordDialogCreation(chrome::DialogIdentifier::DESKTOP_MEDIA_PICKER);
......@@ -281,7 +279,7 @@ gfx::Size DesktopMediaPickerDialogView::CalculatePreferredSize() const {
}
ui::ModalType DesktopMediaPickerDialogView::GetModalType() const {
return ui::MODAL_TYPE_CHILD;
return modality_;
}
base::string16 DesktopMediaPickerDialogView::GetWindowTitle() const {
......@@ -426,18 +424,12 @@ DesktopMediaPickerViews::~DesktopMediaPickerViews() {
}
void DesktopMediaPickerViews::Show(
content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
const DesktopMediaPicker::Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) {
callback_ = done_callback;
dialog_ = new DesktopMediaPickerDialogView(
web_contents, context, this, app_name, target_name,
std::move(source_lists), request_audio);
dialog_ =
new DesktopMediaPickerDialogView(params, this, std::move(source_lists));
}
void DesktopMediaPickerViews::NotifyDialogResult(DesktopMediaID source) {
......
......@@ -25,13 +25,9 @@ class DesktopMediaPickerDialogView : public views::DialogDelegateView,
public views::TabbedPaneListener {
public:
DesktopMediaPickerDialogView(
content::WebContents* parent_web_contents,
gfx::NativeWindow context,
const DesktopMediaPicker::Params& params,
DesktopMediaPickerViews* parent,
const base::string16& app_name,
const base::string16& target_name,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio);
std::vector<std::unique_ptr<DesktopMediaList>> source_lists);
~DesktopMediaPickerDialogView() override;
// Called by parent (DesktopMediaPickerViews) when it's destroyed.
......@@ -71,6 +67,7 @@ class DesktopMediaPickerDialogView : public views::DialogDelegateView,
void OnSourceTypeSwitched(int index);
DesktopMediaPickerViews* parent_;
ui::ModalType modality_;
views::Label* description_label_;
......@@ -92,13 +89,8 @@ class DesktopMediaPickerViews : public DesktopMediaPicker {
void NotifyDialogResult(content::DesktopMediaID source);
// DesktopMediaPicker overrides.
void Show(content::WebContents* web_contents,
gfx::NativeWindow context,
gfx::NativeWindow parent,
const base::string16& app_name,
const base::string16& target_name,
void Show(const DesktopMediaPicker::Params& params,
std::vector<std::unique_ptr<DesktopMediaList>> source_lists,
bool request_audio,
const DoneCallback& done_callback) override;
DesktopMediaPickerDialogView* GetDialogViewForTesting() const {
......
......@@ -33,10 +33,14 @@ class DesktopMediaPickerViewsBrowserTest : public DialogBrowserTest {
source_lists.push_back(std::make_unique<FakeDesktopMediaList>(type));
}
picker_->Show(web_contents, native_window, nullptr,
base::ASCIIToUTF16("app_name"),
base::ASCIIToUTF16("target_name"), std::move(source_lists),
true, DesktopMediaPicker::DoneCallback());
DesktopMediaPicker::Params picker_params;
picker_params.web_contents = web_contents;
picker_params.context = native_window;
picker_params.app_name = base::ASCIIToUTF16("app_name");
picker_params.target_name = base::ASCIIToUTF16("target_name");
picker_params.request_audio = true;
picker_->Show(picker_params, std::move(source_lists),
DesktopMediaPicker::DoneCallback());
}
private:
......
......@@ -55,8 +55,12 @@ class DesktopMediaPickerViewsTest : public testing::Test {
base::string16 app_name = base::ASCIIToUTF16("foo");
picker_views_.reset(new DesktopMediaPickerViews());
picker_views_->Show(nullptr, test_helper_.GetContext(), nullptr, app_name,
app_name, std::move(source_lists), true,
DesktopMediaPicker::Params picker_params;
picker_params.context = test_helper_.GetContext();
picker_params.app_name = app_name;
picker_params.target_name = app_name;
picker_params.request_audio = true;
picker_views_->Show(picker_params, std::move(source_lists),
base::Bind(&DesktopMediaPickerViewsTest::OnPickerDone,
base::Unretained(this)));
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment