File: permission_wizard.mm

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (584 lines) | stat: -rw-r--r-- 21,083 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/host/mac/permission_wizard.h"

#import <Cocoa/Cocoa.h>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "remoting/base/string_resources.h"
#include "ui/base/cocoa/window_size_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"

using remoting::mac::PermissionWizard;
using Delegate = PermissionWizard::Delegate;
using ResultCallback = PermissionWizard::ResultCallback;

namespace {

// Interval between permission checks, used to update the UI when the user
// grants permission.
constexpr base::TimeDelta kPollingInterval = base::Seconds(1);

// The steps of the wizard.
enum class WizardPage {
  ACCESSIBILITY,
  SCREEN_RECORDING,
  ALL_SET,
};

}  // namespace

@interface PermissionWizardController : NSWindowController

- (instancetype)initWithWindow:(NSWindow*)window
                          impl:(PermissionWizard::Impl*)impl;
- (void)hide;
- (void)start;

// Used by C++ PermissionWizardImpl to provide the result of a permission check
// to the WindowController.
- (void)onPermissionCheckResult:(bool)result;

@end

namespace remoting::mac {

// C++ implementation of the PermissionWizard.
class PermissionWizard::Impl {
 public:
  explicit Impl(std::unique_ptr<PermissionWizard::Delegate> checker);
  ~Impl();

  void SetCompletionCallback(ResultCallback callback);
  void Start();

  std::string GetBundleName();

  // Called by PermissionWizardController to initiate permission checks. The
  // result will be passed back via onPermissionCheckResult().
  void CheckAccessibilityPermission(base::TimeDelta delay);
  void CheckScreenRecordingPermission(base::TimeDelta delay);

  // Called by PermissionWizardController to notify that the wizard was
  // completed/cancelled.
  void NotifyCompletion(bool result);

 private:
  void CheckAccessibilityPermissionNow();
  void CheckScreenRecordingPermissionNow();

  void OnPermissionCheckResult(bool result);

  PermissionWizardController* __strong window_controller_ = nil;
  std::unique_ptr<Delegate> checker_;
  base::OneShotTimer timer_;

  // Notified when the wizard is completed/cancelled. May be null.
  ResultCallback completion_callback_;

  base::WeakPtrFactory<Impl> weak_factory_{this};
};

PermissionWizard::Impl::Impl(
    std::unique_ptr<PermissionWizard::Delegate> checker)
    : checker_(std::move(checker)) {}

PermissionWizard::Impl::~Impl() {
  [window_controller_ hide];
  window_controller_ = nil;
}

void PermissionWizard::Impl::SetCompletionCallback(ResultCallback callback) {
  completion_callback_ = std::move(callback);
}

void PermissionWizard::Impl::Start() {
  NSWindow* window =
      [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
                                  styleMask:NSWindowStyleMaskTitled
                                    backing:NSBackingStoreBuffered
                                      defer:NO];
  window.releasedWhenClosed = NO;
  window_controller_ = [[PermissionWizardController alloc] initWithWindow:window
                                                                     impl:this];
  [window_controller_ start];
}

std::string PermissionWizard::Impl::GetBundleName() {
  return checker_->GetBundleName();
}

void PermissionWizard::Impl::CheckAccessibilityPermission(
    base::TimeDelta delay) {
  timer_.Start(FROM_HERE, delay, this, &Impl::CheckAccessibilityPermissionNow);
}

void PermissionWizard::Impl::CheckScreenRecordingPermission(
    base::TimeDelta delay) {
  timer_.Start(FROM_HERE, delay, this,
               &Impl::CheckScreenRecordingPermissionNow);
}

void PermissionWizard::Impl::NotifyCompletion(bool result) {
  if (completion_callback_) {
    std::move(completion_callback_).Run(result);
  }
}

void PermissionWizard::Impl::CheckAccessibilityPermissionNow() {
  checker_->CheckAccessibilityPermission(base::BindOnce(
      &Impl::OnPermissionCheckResult, weak_factory_.GetWeakPtr()));
}

void PermissionWizard::Impl::CheckScreenRecordingPermissionNow() {
  checker_->CheckScreenRecordingPermission(base::BindOnce(
      &Impl::OnPermissionCheckResult, weak_factory_.GetWeakPtr()));
}

void PermissionWizard::Impl::OnPermissionCheckResult(bool result) {
  [window_controller_ onPermissionCheckResult:result];
}

}  // namespace remoting::mac

@implementation PermissionWizardController {
  NSTextField* __strong _instructionText;
  NSButton* __strong _cancelButton;
  NSButton* __strong _launchA11yButton;
  NSButton* __strong _launchScreenRecordingButton;
  NSButton* __strong _nextButton;
  NSButton* __strong _okButton;

  // This class modifies the NSApplicationActivationPolicy in order to show a
  // Dock icon when presenting the dialog window. This is needed because the
  // native-messaging host sets LSUIElement=YES in its plist to hide the Dock
  // icon. This field stores the previous setting so it can be restored when
  // the window is closed (so this class will still do the right thing if it is
  // instantiated from an app that normally shows a Dock icon).
  NSApplicationActivationPolicy _originalActivationPolicy;

  // The page of the wizard being shown.
  WizardPage _page;

  // Whether the relevant permission has been granted for the current page. If
  // YES, the user will be able to advance to the next page of the wizard.
  BOOL _hasPermission;

  // Set to YES when the user cancels the wizard. This allows code to
  // distinguish between "window hidden because it hasn't been presented yet"
  // and "window hidden because user closed it".
  BOOL _cancelled;

  // If YES, the wizard will automatically move onto the next page instead of
  // showing the "Next" button when permission is granted. This allows the
  // wizard to skip past any pages whose permission is already granted.
  BOOL _autoAdvance;

  // Reference used for permission-checking. Its lifetime should outlast this
  // Controller.
  raw_ptr<PermissionWizard::Impl> _impl;
}

- (instancetype)initWithWindow:(NSWindow*)window
                          impl:(PermissionWizard::Impl*)impl {
  DCHECK(window);
  DCHECK(impl);
  self = [super initWithWindow:window];
  if (self) {
    _impl = impl;
    _page = WizardPage::ACCESSIBILITY;
    _autoAdvance = YES;
  }
  _originalActivationPolicy = [NSApp activationPolicy];
  return self;
}

- (void)hide {
  [NSApp setActivationPolicy:_originalActivationPolicy];
  [self close];
}

- (void)start {
  [self initializeWindow];

  // Start polling for permission status.
  [self requestPermissionCheck:base::TimeDelta()];
}

- (void)initializeWindow {
  self.window.title =
      l10n_util::GetNSStringF(IDS_MAC_PERMISSION_WIZARD_TITLE,
                              l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));

  _instructionText = [[NSTextField alloc] init];
  _instructionText.translatesAutoresizingMaskIntoConstraints = NO;
  _instructionText.drawsBackground = NO;
  _instructionText.bezeled = NO;
  _instructionText.editable = NO;
  _instructionText.preferredMaxLayoutWidth = 400;

  NSString* appPath = NSBundle.mainBundle.bundlePath;
  NSImage* iconImage = [NSWorkspace.sharedWorkspace iconForFile:appPath];
  [iconImage setSize:NSMakeSize(64, 64)];
  NSImageView* icon = [[NSImageView alloc] init];
  icon.translatesAutoresizingMaskIntoConstraints = NO;
  icon.image = iconImage;

  _cancelButton = [[NSButton alloc] init];
  _cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
  _cancelButton.buttonType = NSButtonTypeMomentaryPushIn;
  _cancelButton.bezelStyle = NSBezelStyleFlexiblePush;
  _cancelButton.title =
      l10n_util::GetNSString(IDS_MAC_PERMISSION_WIZARD_CANCEL_BUTTON);
  _cancelButton.keyEquivalent = @"\e";
  _cancelButton.action = @selector(onCancel:);
  _cancelButton.target = self;

  _launchA11yButton = [[NSButton alloc] init];
  _launchA11yButton.translatesAutoresizingMaskIntoConstraints = NO;
  _launchA11yButton.buttonType = NSButtonTypeMomentaryPushIn;
  _launchA11yButton.bezelStyle = NSBezelStyleFlexiblePush;
  _launchA11yButton.title =
      l10n_util::GetNSString(IDS_ACCESSIBILITY_PERMISSION_DIALOG_OPEN_BUTTON);
  _launchA11yButton.action = @selector(onLaunchA11y:);
  _launchA11yButton.target = self;

  _launchScreenRecordingButton = [[NSButton alloc] init];
  _launchScreenRecordingButton.translatesAutoresizingMaskIntoConstraints = NO;
  _launchScreenRecordingButton.buttonType = NSButtonTypeMomentaryPushIn;
  _launchScreenRecordingButton.bezelStyle = NSBezelStyleFlexiblePush;
  _launchScreenRecordingButton.title = l10n_util::GetNSString(
      IDS_SCREEN_RECORDING_PERMISSION_DIALOG_OPEN_BUTTON);
  _launchScreenRecordingButton.action = @selector(onLaunchScreenRecording:);
  _launchScreenRecordingButton.target = self;

  _nextButton = [[NSButton alloc] init];
  _nextButton.translatesAutoresizingMaskIntoConstraints = NO;
  _nextButton.buttonType = NSButtonTypeMomentaryPushIn;
  _nextButton.bezelStyle = NSBezelStyleFlexiblePush;
  _nextButton.title =
      l10n_util::GetNSString(IDS_MAC_PERMISSION_WIZARD_NEXT_BUTTON);
  _nextButton.keyEquivalent = @"\r";
  _nextButton.action = @selector(onNext:);
  _nextButton.target = self;

  _okButton = [[NSButton alloc] init];
  _okButton.translatesAutoresizingMaskIntoConstraints = NO;
  _okButton.buttonType = NSButtonTypeMomentaryPushIn;
  _okButton.bezelStyle = NSBezelStyleFlexiblePush;
  _okButton.title = l10n_util::GetNSString(IDS_MAC_PERMISSION_WIZARD_OK_BUTTON);
  _okButton.keyEquivalent = @"\r";
  _okButton.action = @selector(onOk:);
  _okButton.target = self;

  NSStackView* iconAndTextStack = [[NSStackView alloc] init];
  iconAndTextStack.translatesAutoresizingMaskIntoConstraints = NO;
  iconAndTextStack.orientation = NSUserInterfaceLayoutOrientationHorizontal;
  iconAndTextStack.alignment = NSLayoutAttributeTop;
  [iconAndTextStack addView:icon inGravity:NSStackViewGravityLeading];
  [iconAndTextStack addView:_instructionText
                  inGravity:NSStackViewGravityCenter];

  NSStackView* buttonsStack = [[NSStackView alloc] init];
  buttonsStack.translatesAutoresizingMaskIntoConstraints = NO;
  buttonsStack.orientation = NSUserInterfaceLayoutOrientationHorizontal;
  [buttonsStack addView:_cancelButton inGravity:NSStackViewGravityTrailing];
  [buttonsStack addView:_launchA11yButton inGravity:NSStackViewGravityTrailing];
  [buttonsStack addView:_launchScreenRecordingButton
              inGravity:NSStackViewGravityTrailing];
  [buttonsStack addView:_nextButton inGravity:NSStackViewGravityTrailing];
  [buttonsStack addView:_okButton inGravity:NSStackViewGravityTrailing];

  // Prevent buttonsStack from expanding vertically. This fixes incorrect
  // vertical placement of OK button in All Set page
  // (http://crbug.com/1032157). The parent NSStackView was expanding this
  // view instead of adding space between the gravity-areas.
  [buttonsStack setHuggingPriority:NSLayoutPriorityDefaultHigh
                    forOrientation:NSLayoutConstraintOrientationVertical];

  NSStackView* mainStack = [[NSStackView alloc] init];
  mainStack.translatesAutoresizingMaskIntoConstraints = NO;
  mainStack.orientation = NSUserInterfaceLayoutOrientationVertical;
  mainStack.spacing = 12;
  [mainStack addView:iconAndTextStack inGravity:NSStackViewGravityTop];
  [mainStack addView:buttonsStack inGravity:NSStackViewGravityBottom];

  [self.window.contentView addSubview:mainStack];

  // Update button visibility, instructional text etc before window is
  // presented, to ensure correct layout. This updates the window's
  // first-responder, so it needs to happen after the child views are added to
  // the contentView.
  [self updateUI];

  NSDictionary* views = @{
    @"iconAndText" : iconAndTextStack,
    @"buttons" : buttonsStack,
    @"mainStack" : mainStack,
  };

  // Expand |iconAndTextStack| to match parent's width.
  [mainStack addConstraints:[NSLayoutConstraint
                                constraintsWithVisualFormat:@"H:|[iconAndText]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];

  // Expand |buttonsStack| to match parent's width.
  [mainStack addConstraints:[NSLayoutConstraint
                                constraintsWithVisualFormat:@"H:|[buttons]|"
                                                    options:0
                                                    metrics:nil
                                                      views:views]];

  // Expand |mainStack| to fill the window's contentView (with standard margin).
  [self.window.contentView
      addConstraints:[NSLayoutConstraint
                         constraintsWithVisualFormat:@"H:|-[mainStack]-|"
                                             options:0
                                             metrics:nil
                                               views:views]];
  [self.window.contentView
      addConstraints:[NSLayoutConstraint
                         constraintsWithVisualFormat:@"V:|-[mainStack]-|"
                                             options:0
                                             metrics:nil
                                               views:views]];
}

- (void)onCancel:(id)sender {
  _impl->NotifyCompletion(false);
  _cancelled = YES;
  [self hide];
}

- (void)onLaunchA11y:(id)sender {
  base::mac::OpenSystemSettingsPane(
      base::mac::SystemSettingsPane::kPrivacySecurity_Accessibility);
}

- (void)onLaunchScreenRecording:(id)sender {
  base::mac::OpenSystemSettingsPane(
      base::mac::SystemSettingsPane::kPrivacySecurity_ScreenRecording);
}

- (void)onNext:(id)sender {
  [self advanceToNextPage];
}

- (void)onOk:(id)sender {
  // OK button closes the window.
  _impl->NotifyCompletion(true);
  [self hide];
}

// Updates the dialog controls according to the object's state. This also
// updates the first-responder button, so it should only be called when the
// state needs to change.
- (void)updateUI {
  std::u16string bundleName = base::UTF8ToUTF16(_impl->GetBundleName());
  switch (_page) {
    case WizardPage::ACCESSIBILITY:
      _instructionText.stringValue = l10n_util::GetNSStringF(
          IDS_ACCESSIBILITY_PERMISSION_DIALOG_BODY_TEXT,
          l10n_util::GetStringUTF16(IDS_PRODUCT_NAME),
          l10n_util::GetStringUTF16(
              IDS_ACCESSIBILITY_PERMISSION_DIALOG_OPEN_BUTTON),
          bundleName);
      break;
    case WizardPage::SCREEN_RECORDING:
      _instructionText.stringValue = l10n_util::GetNSStringF(
          IDS_SCREEN_RECORDING_PERMISSION_DIALOG_BODY_TEXT,
          l10n_util::GetStringUTF16(IDS_PRODUCT_NAME),
          l10n_util::GetStringUTF16(
              IDS_SCREEN_RECORDING_PERMISSION_DIALOG_OPEN_BUTTON),
          bundleName);
      break;
    case WizardPage::ALL_SET:
      _instructionText.stringValue =
          l10n_util::GetNSString(IDS_MAC_PERMISSION_WIZARD_FINAL_TEXT);
      break;
    default:
      NOTREACHED();
  }
  [self updateButtons];
}

// Updates the buttons according to the object's state. This updates the
// first-responder, so this should only be called when the buttons need to be
// changed.
- (void)updateButtons {
  // Launch buttons are always visible on their associated pages.
  _launchA11yButton.hidden = (_page != WizardPage::ACCESSIBILITY);
  _launchScreenRecordingButton.hidden = (_page != WizardPage::SCREEN_RECORDING);

  // OK is visible on ALL_SET, Cancel/Next are visible on all other pages.
  _cancelButton.hidden = (_page == WizardPage::ALL_SET);
  _nextButton.hidden = (_page == WizardPage::ALL_SET);
  _okButton.hidden = (_page != WizardPage::ALL_SET);

  // User can only advance if permission is granted.
  _nextButton.enabled = _hasPermission;

  // Give focus to the most appropriate button.
  if (_page == WizardPage::ALL_SET) {
    [self.window makeFirstResponder:_okButton];
  } else if (_hasPermission) {
    [self.window makeFirstResponder:_nextButton];
  } else {
    switch (_page) {
      case WizardPage::ACCESSIBILITY:
        [self.window makeFirstResponder:_launchA11yButton];
        break;
      case WizardPage::SCREEN_RECORDING:
        [self.window makeFirstResponder:_launchScreenRecordingButton];
        break;
      default:
        NOTREACHED();
    }
  }

  // Set the button tab-order (key view loop). Hidden/disabled buttons are
  // skipped, so it is OK to set the overall order for every button. This needs
  // to be done after setting the first-responder, otherwise the system chooses
  // an order which may not be correct.
  _cancelButton.nextKeyView = _launchA11yButton;
  _launchA11yButton.nextKeyView = _launchScreenRecordingButton;
  _launchScreenRecordingButton.nextKeyView = _nextButton;
  _nextButton.nextKeyView = _okButton;
  _okButton.nextKeyView = _cancelButton;
}

- (void)advanceToNextPage {
  DCHECK(_hasPermission);
  switch (_page) {
    case WizardPage::ACCESSIBILITY:
      _page = WizardPage::SCREEN_RECORDING;
      break;
    case WizardPage::SCREEN_RECORDING:
      _page = WizardPage::ALL_SET;
      if ([self window].visible) {
        [self updateUI];
      } else {
        // If the wizard hasn't been shown yet, this means that all permissions
        // were already granted, and the final ALL_SET page will not be shown.
        _impl->NotifyCompletion(true);
      }
      return;
    default:
      NOTREACHED();
  }

  // Kick off a permission check for the new page. Update the UI now, so the
  // Next button is disabled and can't be accidentally double-pressed.
  _hasPermission = NO;
  _autoAdvance = YES;
  [self updateUI];
  [self requestPermissionCheck:base::TimeDelta()];
}

- (void)requestPermissionCheck:(base::TimeDelta)delay {
  DCHECK(!_hasPermission);
  switch (_page) {
    case WizardPage::ACCESSIBILITY:
      _impl->CheckAccessibilityPermission(delay);
      break;
    case WizardPage::SCREEN_RECORDING:
      _impl->CheckScreenRecordingPermission(delay);
      return;
    default:
      NOTREACHED();
  }
}

- (void)onPermissionCheckResult:(bool)result {
  if (_cancelled) {
    return;
  }

  _hasPermission = result;

  if (_hasPermission && _autoAdvance) {
    // Skip showing the "Next" button, and immediately kick off a permission
    // check for the next page, if any.
    [self advanceToNextPage];
    return;
  }

  // Don't update the UI if permission denied, because that resets the button
  // focus, preventing the user from tabbing between buttons while polling for
  // permission status.
  if (_hasPermission) {
    // Update the whole UI, not just the "Next" button, in case a different page
    // was previously shown.
    [self updateUI];

    // Bring the window to the front again, to prompt the user to hit Next.
    [self presentWindow];
  } else {
    // Permission denied, so turn off auto-advance for this page, and present
    // the dialog to the user if needed. After the user grants this permission,
    // they should be able to click "Next" to acknowledge and advance the
    // wizard. Note that, if all permissions are granted, the user will not
    // see the wizard at all (not even the ALL_SET page). A dialog is only
    // shown when a permission-check fails.
    _autoAdvance = NO;
    if (![self window].visible) {
      // Only present the window if it was previously hidden. This method will
      // bring the window on top of other windows, which should not happen
      // during regular polling for permission status, as the user is focused on
      // the System Preferences applet.
      [self presentWindow];
    }

    // Keep polling until permission is granted.
    [self requestPermissionCheck:kPollingInterval];
  }
}

- (void)presentWindow {
  [self.window makeKeyAndOrderFront:NSApp];
  [self.window center];
  [self showWindow:nil];
  [NSApp activateIgnoringOtherApps:YES];

  // Show the application icon in the dock.
  [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}

@end

namespace remoting::mac {

PermissionWizard::PermissionWizard(std::unique_ptr<Delegate> checker)
    : impl_(std::make_unique<PermissionWizard::Impl>(std::move(checker))) {}

PermissionWizard::~PermissionWizard() {
  ui_task_runner_->DeleteSoon(FROM_HERE, impl_.release());
}

void PermissionWizard::SetCompletionCallback(ResultCallback callback) {
  impl_->SetCompletionCallback(std::move(callback));
}

void PermissionWizard::Start(
    scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
  ui_task_runner_ = ui_task_runner;
  ui_task_runner->PostTask(
      FROM_HERE, base::BindOnce(&Impl::Start, base::Unretained(impl_.get())));
}

}  // namespace remoting::mac