Commit ae209392 authored by courage@chromium.org's avatar courage@chromium.org

remove oauth from extension install dialog

The chrome.identity.getAuthToken API now uses a totally separate dialog for displaying its permissions, so the ExtensionInstallDialog implementation can be deleted.

BUG=304836

Review URL: https://codereview.chromium.org/290733006

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@272365 0039d316-1c4b-4281-b951-d872f2087c98
parent b8c85c2e
......@@ -18,13 +18,10 @@
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/image_loader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/api/identity/oauth2_manifest_handler.h"
#include "chrome/common/pref_names.h"
#include "components/signin/core/browser/profile_oauth2_token_service.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_util.h"
......@@ -121,17 +118,6 @@ static const int
IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO,
IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO,
};
static const int kOAuthHeaderIds[ExtensionInstallPrompt::NUM_PROMPT_TYPES] = {
IDS_EXTENSION_PROMPT_OAUTH_HEADER,
0, // Inline installs don't show OAuth permissions.
0, // Bundle installs don't show OAuth permissions.
IDS_EXTENSION_PROMPT_OAUTH_REENABLE_HEADER,
IDS_EXTENSION_PROMPT_OAUTH_PERMISSIONS_HEADER,
0,
0,
IDS_EXTENSION_PROMPT_OAUTH_HEADER,
IDS_EXTENSION_PROMPT_OAUTH_HEADER,
};
// Size of extension icon in top left of dialog.
const int kIconSize = 69;
......@@ -235,34 +221,12 @@ void ExtensionInstallPrompt::Prompt::SetIsShowingDetails(
case PERMISSIONS_DETAILS:
is_showing_details_for_permissions_[index] = is_showing_details;
break;
case OAUTH_DETAILS:
is_showing_details_for_oauth_[index] = is_showing_details;
break;
case RETAINED_FILES_DETAILS:
is_showing_details_for_retained_files_ = is_showing_details;
break;
}
}
void ExtensionInstallPrompt::Prompt::SetOAuthIssueAdvice(
const IssueAdviceInfo& issue_advice) {
is_showing_details_for_oauth_.clear();
for (size_t i = 0; i < issue_advice.size(); ++i)
is_showing_details_for_oauth_.push_back(false);
oauth_issue_advice_ = issue_advice;
}
void ExtensionInstallPrompt::Prompt::SetUserNameFromProfile(Profile* profile) {
// |profile| can be NULL in unit tests.
if (profile) {
oauth_user_name_ = base::UTF8ToUTF16(profile->GetPrefs()->GetString(
prefs::kGoogleServicesUsername));
} else {
oauth_user_name_.clear();
}
}
void ExtensionInstallPrompt::Prompt::SetWebstoreData(
const std::string& localized_user_count,
bool show_user_count,
......@@ -376,10 +340,6 @@ base::string16 ExtensionInstallPrompt::Prompt::GetPermissionsHeading() const {
return l10n_util::GetStringUTF16(kPermissionsHeaderIds[type_]);
}
base::string16 ExtensionInstallPrompt::Prompt::GetOAuthHeading() const {
return l10n_util::GetStringFUTF16(kOAuthHeaderIds[type_], oauth_user_name_);
}
base::string16 ExtensionInstallPrompt::Prompt::GetRetainedFilesHeading() const {
const int kRetainedFilesMessageIDs[6] = {
IDS_EXTENSION_PROMPT_RETAINED_FILES_DEFAULT,
......@@ -471,25 +431,12 @@ bool ExtensionInstallPrompt::Prompt::GetIsShowingDetails(
case PERMISSIONS_DETAILS:
CHECK_LT(index, is_showing_details_for_permissions_.size());
return is_showing_details_for_permissions_[index];
case OAUTH_DETAILS:
CHECK_LT(index, is_showing_details_for_oauth_.size());
return is_showing_details_for_oauth_[index];
case RETAINED_FILES_DETAILS:
return is_showing_details_for_retained_files_;
}
return false;
}
size_t ExtensionInstallPrompt::Prompt::GetOAuthIssueCount() const {
return oauth_issue_advice_.size();
}
const IssueAdviceInfoEntry& ExtensionInstallPrompt::Prompt::GetOAuthIssue(
size_t index) const {
CHECK_LT(index, oauth_issue_advice_.size());
return oauth_issue_advice_[index];
}
size_t ExtensionInstallPrompt::Prompt::GetRetainedFileCount() const {
return retained_files_.size();
}
......@@ -550,33 +497,25 @@ scoped_refptr<Extension>
}
ExtensionInstallPrompt::ExtensionInstallPrompt(content::WebContents* contents)
: OAuth2TokenService::Consumer("extensions_install"),
record_oauth2_grant_(false),
ui_loop_(base::MessageLoop::current()),
: ui_loop_(base::MessageLoop::current()),
extension_(NULL),
bundle_(NULL),
install_ui_(ExtensionInstallUI::Create(ProfileForWebContents(contents))),
show_params_(contents),
delegate_(NULL),
prompt_(UNSET_PROMPT_TYPE) {
prompt_.SetUserNameFromProfile(install_ui_->profile());
}
prompt_(UNSET_PROMPT_TYPE) {}
ExtensionInstallPrompt::ExtensionInstallPrompt(
Profile* profile,
gfx::NativeWindow native_window,
content::PageNavigator* navigator)
: OAuth2TokenService::Consumer("extensions_install"),
record_oauth2_grant_(false),
ui_loop_(base::MessageLoop::current()),
: ui_loop_(base::MessageLoop::current()),
extension_(NULL),
bundle_(NULL),
install_ui_(ExtensionInstallUI::Create(profile)),
show_params_(native_window, navigator),
delegate_(NULL),
prompt_(UNSET_PROMPT_TYPE) {
prompt_.SetUserNameFromProfile(install_ui_->profile());
}
prompt_(UNSET_PROMPT_TYPE) {}
ExtensionInstallPrompt::~ExtensionInstallPrompt() {
}
......@@ -698,21 +637,6 @@ void ExtensionInstallPrompt::ConfirmPermissions(
LoadImageIfNeeded();
}
void ExtensionInstallPrompt::ConfirmIssueAdvice(
Delegate* delegate,
const Extension* extension,
const IssueAdviceInfo& issue_advice) {
DCHECK(ui_loop_ == base::MessageLoop::current());
extension_ = extension;
delegate_ = delegate;
prompt_.set_type(PERMISSIONS_PROMPT);
record_oauth2_grant_ = true;
prompt_.SetOAuthIssueAdvice(issue_advice);
LoadImageIfNeeded();
}
void ExtensionInstallPrompt::ReviewPermissions(
Delegate* delegate,
const Extension* extension,
......@@ -781,48 +705,6 @@ void ExtensionInstallPrompt::LoadImageIfNeeded() {
base::Bind(&ExtensionInstallPrompt::OnImageLoaded, AsWeakPtr()));
}
void ExtensionInstallPrompt::OnGetTokenSuccess(
const OAuth2TokenService::Request* request,
const std::string& access_token,
const base::Time& expiration_time) {
DCHECK_EQ(login_token_request_.get(), request);
login_token_request_.reset();
const extensions::OAuth2Info& oauth2_info =
extensions::OAuth2Info::GetOAuth2Info(extension_);
token_flow_.reset(new OAuth2MintTokenFlow(
install_ui_->profile()->GetRequestContext(),
this,
OAuth2MintTokenFlow::Parameters(
access_token,
extension_->id(),
oauth2_info.client_id,
oauth2_info.scopes,
OAuth2MintTokenFlow::MODE_ISSUE_ADVICE)));
token_flow_->Start();
}
void ExtensionInstallPrompt::OnGetTokenFailure(
const OAuth2TokenService::Request* request,
const GoogleServiceAuthError& error) {
DCHECK_EQ(login_token_request_.get(), request);
login_token_request_.reset();
ShowConfirmation();
}
void ExtensionInstallPrompt::OnIssueAdviceSuccess(
const IssueAdviceInfo& advice_info) {
prompt_.SetOAuthIssueAdvice(advice_info);
record_oauth2_grant_ = true;
ShowConfirmation();
}
void ExtensionInstallPrompt::OnMintTokenFailure(
const GoogleServiceAuthError& error) {
ShowConfirmation();
}
void ExtensionInstallPrompt::ShowConfirmation() {
if (prompt_.type() == INSTALL_PROMPT)
prompt_.set_experiment(ExtensionInstallPromptExperiment::Find());
......
......@@ -12,12 +12,11 @@
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string16.h"
#include "chrome/browser/extensions/crx_installer_error.h"
#include "chrome/browser/extensions/extension_install_prompt_experiment.h"
#include "extensions/common/url_pattern.h"
#include "google_apis/gaia/oauth2_mint_token_flow.h"
#include "google_apis/gaia/oauth2_token_service.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
......@@ -51,9 +50,7 @@ class InfoBarDelegate;
// Displays all the UI around extension installation.
class ExtensionInstallPrompt
: public OAuth2MintTokenFlow::Delegate,
public OAuth2TokenService::Consumer,
public base::SupportsWeakPtr<ExtensionInstallPrompt> {
: public base::SupportsWeakPtr<ExtensionInstallPrompt> {
public:
// This enum is associated with Extensions.InstallPrompt_Type UMA histogram.
// Do not modify existing values and add new values only to the end.
......@@ -73,7 +70,6 @@ class ExtensionInstallPrompt
enum DetailsType {
PERMISSIONS_DETAILS = 0,
OAUTH_DETAILS,
RETAINED_FILES_DETAILS,
};
......@@ -97,7 +93,6 @@ class ExtensionInstallPrompt
bool show_user_count,
double average_rating,
int rating_count);
void SetOAuthIssueAdvice(const IssueAdviceInfo& issue_advice);
void SetUserNameFromProfile(Profile* profile);
PromptType type() const { return type_; }
......@@ -112,7 +107,6 @@ class ExtensionInstallPrompt
bool HasAbortButtonLabel() const;
base::string16 GetAbortButtonLabel() const;
base::string16 GetPermissionsHeading() const;
base::string16 GetOAuthHeading() const;
base::string16 GetRetainedFilesHeading() const;
bool ShouldShowPermissions() const;
......@@ -134,8 +128,6 @@ class ExtensionInstallPrompt
base::string16 GetPermission(size_t index) const;
base::string16 GetPermissionsDetails(size_t index) const;
bool GetIsShowingDetails(DetailsType type, size_t index) const;
size_t GetOAuthIssueCount() const;
const IssueAdviceInfoEntry& GetOAuthIssue(size_t index) const;
size_t GetRetainedFileCount() const;
base::string16 GetRetainedFile(size_t index) const;
......@@ -178,16 +170,8 @@ class ExtensionInstallPrompt
std::vector<base::string16> permissions_;
std::vector<base::string16> details_;
std::vector<bool> is_showing_details_for_permissions_;
std::vector<bool> is_showing_details_for_oauth_;
bool is_showing_details_for_retained_files_;
// Descriptions and details for OAuth2 permissions to display to the user.
// These correspond to permission scopes.
IssueAdviceInfo oauth_issue_advice_;
// User name to be used in Oauth heading label.
base::string16 oauth_user_name_;
// The extension or bundle being installed.
const extensions::Extension* extension_;
const extensions::BundleInstaller* bundle_;
......@@ -279,8 +263,6 @@ class ExtensionInstallPrompt
ExtensionInstallUI* install_ui() const { return install_ui_.get(); }
bool record_oauth2_grant() const { return record_oauth2_grant_; }
content::WebContents* parent_web_contents() const {
return show_params_.parent_web_contents;
}
......@@ -347,14 +329,6 @@ class ExtensionInstallPrompt
const extensions::Extension* extension,
const extensions::PermissionSet* permissions);
// This is called by the extension identity API to verify whether an
// extension can be granted an OAuth2 token.
//
// We *MUST* eventually call either Proceed() or Abort() on |delegate|.
virtual void ConfirmIssueAdvice(Delegate* delegate,
const extensions::Extension* extension,
const IssueAdviceInfo& issue_advice);
// This is called by the app handler launcher to review what permissions the
// extension or app currently has.
//
......@@ -373,7 +347,6 @@ class ExtensionInstallPrompt
protected:
friend class extensions::ExtensionWebstorePrivateApiTest;
friend class extensions::MockGetAuthTokenFunction;
friend class WebstoreStartupInstallUnpackFailureTest;
// Whether or not we should record the oauth2 grant upon successful install.
......@@ -394,19 +367,6 @@ class ExtensionInstallPrompt
// 2) Handle the load icon response and show the UI (OnImageLoaded).
void LoadImageIfNeeded();
// OAuth2TokenService::Consumer implementation:
virtual void OnGetTokenSuccess(const OAuth2TokenService::Request* request,
const std::string& access_token,
const base::Time& expiration_time) OVERRIDE;
virtual void OnGetTokenFailure(const OAuth2TokenService::Request* request,
const GoogleServiceAuthError& error) OVERRIDE;
// OAuth2MintTokenFlow::Delegate implementation:
virtual void OnIssueAdviceSuccess(
const IssueAdviceInfo& issue_advice) OVERRIDE;
virtual void OnMintTokenFailure(
const GoogleServiceAuthError& error) OVERRIDE;
// Shows the actual UI (the icon should already be loaded).
void ShowConfirmation();
......@@ -437,9 +397,6 @@ class ExtensionInstallPrompt
// A pre-filled prompt.
Prompt prompt_;
scoped_ptr<OAuth2TokenService::Request> login_token_request_;
scoped_ptr<OAuth2MintTokenFlow> token_flow_;
// Used to show the confirm dialog.
ShowDialogCallback show_dialog_callback_;
};
......
......@@ -30,8 +30,7 @@ class PageNavigator;
IBOutlet NSButton* cancelButton_;
IBOutlet NSButton* okButton_;
// Present only when the dialog has permission warnings or OAuth issues to
// display.
// Present only when the dialog has permission warnings issues to display.
IBOutlet NSOutlineView* outlineView_;
// Present only in the install dialogs with webstore data (inline and
......
......@@ -190,7 +190,6 @@ bool HasAttribute(id item, CellAttributesMask attributeMask) {
} else if (prompt.has_webstore_data()) {
nibName = @"ExtensionInstallPromptWebstoreData";
} else if (!prompt.ShouldShowPermissions() &&
prompt.GetOAuthIssueCount() == 0 &&
prompt.GetRetainedFileCount() == 0) {
nibName = @"ExtensionInstallPromptNoWarnings";
} else {
......@@ -308,10 +307,9 @@ bool HasAttribute(id item, CellAttributesMask attributeMask) {
OffsetControlVerticallyToFitContent(itemsField_, &totalOffset);
}
// If there are any warnings or OAuth issues, then we have to do some special
// layout.
if (prompt_->ShouldShowPermissions() || prompt_->GetOAuthIssueCount() > 0 ||
prompt_->GetRetainedFileCount() > 0) {
// If there are any warnings or retained files, then we have to do
// some special layout.
if (prompt_->ShouldShowPermissions() || prompt_->GetRetainedFileCount() > 0) {
NSSize spacing = [outlineView_ intercellSpacing];
spacing.width += 2;
spacing.height += 2;
......@@ -639,41 +637,6 @@ bool HasAttribute(id item, CellAttributesMask attributeMask) {
children:children]];
}
if (prompt.GetOAuthIssueCount() > 0) {
type = ExtensionInstallPrompt::OAUTH_DETAILS;
NSMutableArray* children = [NSMutableArray array];
for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) {
NSMutableArray* details = [NSMutableArray array];
const IssueAdviceInfoEntry& issue = prompt.GetOAuthIssue(i);
if (!issue.details.empty() && prompt.GetIsShowingDetails(type, i)) {
for (size_t j = 0; j < issue.details.size(); ++j) {
[details addObject:
[self buildItemWithTitle:SysUTF16ToNSString(issue.details[j])
cellAttributes:kNoExpandMarker
children:nil]];
}
}
[children addObject:
[self buildItemWithTitle:SysUTF16ToNSString(issue.description)
cellAttributes:kUseBullet | kAutoExpandCell
children:details]];
if (!issue.details.empty()) {
// Add a row for the link.
[children addObject:
[self buildDetailToggleItem:type permissionsDetailIndex:i]];
}
}
[warnings addObject:
[self buildItemWithTitle:SysUTF16ToNSString(prompt.GetOAuthHeading())
cellAttributes:kBoldText | kAutoExpandCell| kNoExpandMarker
children:children]];
}
if (prompt.GetRetainedFileCount() > 0) {
type = ExtensionInstallPrompt::RETAINED_FILES_DETAILS;
......
......@@ -262,48 +262,6 @@ TEST_F(ExtensionInstallViewControllerTest, BasicsInline) {
EXPECT_TRUE([[controller warningsSeparator] isHidden]);
}
TEST_F(ExtensionInstallViewControllerTest, OAuthIssues) {
chrome::MockExtensionInstallPromptDelegate delegate;
ExtensionInstallPrompt::Prompt prompt =
chrome::BuildExtensionInstallPrompt(extension_.get());
std::vector<base::string16> permissions;
permissions.push_back(base::UTF8ToUTF16("warning 1"));
prompt.SetPermissions(permissions);
// No details provided with this permission.
std::vector<base::string16> details;
details.push_back(base::string16());
prompt.SetPermissionsDetails(details);
IssueAdviceInfoEntry issue;
issue.description = base::UTF8ToUTF16("issue description 1");
issue.details.push_back(base::UTF8ToUTF16("issue detail 1"));
IssueAdviceInfo issues;
issues.push_back(issue);
prompt.SetOAuthIssueAdvice(issues);
prompt.SetIsShowingDetails(
ExtensionInstallPrompt::OAUTH_DETAILS, 0, true);
base::scoped_nsobject<ExtensionInstallViewController> controller(
[[ExtensionInstallViewController alloc] initWithNavigator:browser()
delegate:&delegate
prompt:prompt]);
[controller view]; // Force nib load.
NSOutlineView* outlineView = [controller outlineView];
EXPECT_TRUE(outlineView);
EXPECT_EQ(6, [outlineView numberOfRows]);
EXPECT_NSEQ(base::SysUTF16ToNSString(prompt.GetOAuthIssue(0).description),
[[outlineView dataSource] outlineView:outlineView
objectValueForTableColumn:nil
byItem:[outlineView itemAtRow:3]]);
EXPECT_NSEQ(base::SysUTF16ToNSString(prompt.GetOAuthIssue(0).details[0]),
[[outlineView dataSource] outlineView:outlineView
objectValueForTableColumn:nil
byItem:[outlineView itemAtRow:4]]);
}
TEST_F(ExtensionInstallViewControllerTest, PostInstallPermissionsPrompt) {
chrome::MockExtensionInstallPromptDelegate delegate;
......
......@@ -437,7 +437,7 @@ ExtensionInstallDialogView::ExtensionInstallDialogView(
// +--------------------+------+
//
// Regular install
// w/ permissions XOR oauth issues no permissions
// w/ permissions no permissions
// +--------------------+------+ +--------------+------+
// | heading | icon | | heading | icon |
// +--------------------| | +--------------+------+
......@@ -448,23 +448,6 @@ ExtensionInstallDialogView::ExtensionInstallDialogView(
// | permission2 | |
// +--------------------+------+
//
// w/ permissions AND oauth issues
// +--------------------+------+
// | heading | icon |
// +--------------------| |
// | permissions_header | |
// +--------------------| |
// | permission1 | |
// +--------------------| |
// | permission2 | |
// +--------------------+------+
// | oauth header |
// +---------------------------+
// | oauth issue 1 |
// +---------------------------+
// | oauth issue 2 |
// +---------------------------+
//
// If the ExtensionPermissionDialog is on, the layout is modified depending
// on the experiment group. For text only experiment, a footer is added at the
// bottom of the layouts. For others, inline details are added below some of
......@@ -513,7 +496,7 @@ ExtensionInstallDialogView::ExtensionInstallDialogView(
// the dialog depending on the experiment group.
int left_column_width =
(prompt.ShouldShowPermissions() + prompt.GetOAuthIssueCount() +
(prompt.ShouldShowPermissions() +
prompt.GetRetainedFileCount()) > 0 ?
kPermissionsLeftColumnWidth : kNoPermissionsLeftColumnWidth;
if (is_bundle_install())
......@@ -706,54 +689,13 @@ ExtensionInstallDialogView::ExtensionInstallDialogView(
}
}
if (prompt.GetOAuthIssueCount()) {
// Slide in under the permissions, if there are any. If there are
// permissions, the OAuth prompt stretches all the way to the right of the
// dialog. If there are no permissions, the OAuth prompt just takes up the
// left column.
int space_for_oauth = left_column_width;
if (prompt.GetPermissionCount()) {
space_for_oauth += kIconSize;
views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
column_set->AddColumn(views::GridLayout::FILL,
views::GridLayout::FILL,
1,
views::GridLayout::USE_PREF,
0, // no fixed width
space_for_oauth);
}
layout->StartRowWithPadding(0, column_set_id,
0, views::kRelatedControlVerticalSpacing);
views::Label* oauth_header = new views::Label(prompt.GetOAuthHeading());
oauth_header->SetMultiLine(true);
oauth_header->SetHorizontalAlignment(gfx::ALIGN_LEFT);
oauth_header->SizeToFit(left_column_width);
layout->AddView(oauth_header);
for (size_t i = 0; i < prompt.GetOAuthIssueCount(); ++i) {
layout->StartRowWithPadding(
0, column_set_id,
0, views::kRelatedControlVerticalSpacing);
PermissionDetails details;
const IssueAdviceInfoEntry& entry = prompt.GetOAuthIssue(i);
for (size_t x = 0; x < entry.details.size(); ++x)
details.push_back(entry.details[x]);
ExpandableContainerView* issue_advice_view =
new ExpandableContainerView(
this, entry.description, details, space_for_oauth,
true, true, false);
layout->AddView(issue_advice_view);
}
}
if (prompt.GetRetainedFileCount()) {
// Slide in under the permissions or OAuth, if there are any. If there are
// either, the retained files prompt stretches all the way to the right of
// the dialog. If there are no permissions or OAuth, the retained files
// prompt just takes up the left column.
// Slide in under the permissions, if there are any. If there are
// either, the retained files prompt stretches all the way to the
// right of the dialog. If there are no permissions, the retained
// files prompt just takes up the left column.
int space_for_files = left_column_width;
if (prompt.GetPermissionCount() || prompt.GetOAuthIssueCount()) {
if (prompt.GetPermissionCount()) {
space_for_files += kIconSize;
views::ColumnSet* column_set = layout->AddColumnSet(++column_set_id);
column_set->AddColumn(views::GridLayout::FILL,
......@@ -816,10 +758,10 @@ ExtensionInstallDialogView::ExtensionInstallDialogView(
(prompt.experiment()->show_details_link() &&
prompt.experiment()->should_show_inline_explanations() &&
!inline_explanations_.empty())) {
// Don't show the "Show details" link if there are OAuth issues or
// retained files. These have their own "Show details" links and having
// Don't show the "Show details" link if there are retained
// files. These have their own "Show details" links and having
// multiple levels of links is confusing.
if (prompt.GetOAuthIssueCount() + prompt.GetRetainedFileCount() == 0) {
if (prompt.GetRetainedFileCount() == 0) {
int text_id =
prompt.experiment()->should_show_expandable_permission_list() ?
IDS_EXTENSION_PROMPT_EXPERIMENT_SHOW_PERMISSIONS :
......@@ -921,10 +863,6 @@ views::GridLayout* ExtensionInstallDialogView::CreateLayout(
// have a padding row above it). This also works for the 'no special
// permissions' case.
icon_row_span = 3 + permission_count * 2;
} else if (prompt_.GetOAuthIssueCount()) {
// Also span the permission header and each of the permission rows (all
// have a padding row above it).
icon_row_span = 3 + prompt_.GetOAuthIssueCount() * 2;
} else if (prompt_.GetRetainedFileCount()) {
// Also span the permission header and the retained files container.
icon_row_span = 4;
......
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