1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/ui/views/native_file_system/native_file_system_usage_bubble_view.h"
6
7 #include "base/i18n/message_formatter.h"
8 #include "base/i18n/unicodestring.h"
9 #include "base/metrics/user_metrics.h"
10 #include "base/stl_util.h"
11 #include "chrome/app/vector_icons/vector_icons.h"
12 #include "chrome/browser/native_file_system/chrome_native_file_system_permission_context.h"
13 #include "chrome/browser/native_file_system/native_file_system_permission_context_factory.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/browser_finder.h"
16 #include "chrome/browser/ui/browser_window.h"
17 #include "chrome/browser/ui/views/chrome_layout_provider.h"
18 #include "chrome/browser/ui/views/chrome_typography.h"
19 #include "chrome/browser/ui/views/frame/browser_view.h"
20 #include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
21 #include "chrome/browser/ui/views/native_file_system/native_file_system_ui_helpers.h"
22 #include "chrome/browser/ui/views/page_action/page_action_icon_view.h"
23 #include "chrome/grit/generated_resources.h"
24 #include "components/strings/grit/components_strings.h"
25 #include "components/vector_icons/vector_icons.h"
26 #include "content/public/browser/render_frame_host.h"
27 #include "content/public/browser/render_process_host.h"
28 #include "content/public/browser/web_contents.h"
29 #include "third_party/icu/source/common/unicode/unistr.h"
30 #include "third_party/icu/source/common/unicode/utypes.h"
31 #include "third_party/icu/source/i18n/unicode/listformatter.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/gfx/paint_vector_icon.h"
34 #include "ui/views/controls/button/image_button.h"
35 #include "ui/views/controls/button/image_button_factory.h"
36 #include "ui/views/controls/button/label_button.h"
37 #include "ui/views/controls/button/md_text_button.h"
38 #include "ui/views/controls/label.h"
39 #include "ui/views/controls/scroll_view.h"
40 #include "ui/views/controls/table/table_view.h"
41 #include "ui/views/layout/box_layout.h"
42
43 namespace {
44
45 // Returns the message Id to use as heading text, depending on what types of
46 // usage are present (i.e. just writable files, or also readable directories,
47 // etc).
48 // |need_lifetime_text_at_end| is set to false iff the returned message Id
49 // already includes an explanation for how long a website will have access to
50 // the listed paths. It is set to true iff a separate label is needed at the end
51 // of the dialog to explain lifetime.
ComputeHeadingMessageFromUsage(const NativeFileSystemUsageBubbleView::Usage & usage,base::FilePath * embedded_path)52 int ComputeHeadingMessageFromUsage(
53 const NativeFileSystemUsageBubbleView::Usage& usage,
54 base::FilePath* embedded_path) {
55 // Only files.
56 if (usage.writable_directories.empty() &&
57 usage.readable_directories.empty()) {
58 // Only writable files.
59 if (usage.readable_files.empty()) {
60 DCHECK(!usage.writable_files.empty());
61 if (usage.writable_files.size() == 1) {
62 *embedded_path = usage.writable_files.front();
63 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_SINGLE_WRITABLE_FILE_TEXT;
64 }
65 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_WRITABLE_FILES_TEXT;
66 }
67
68 // Only readable files.
69 if (usage.writable_files.empty()) {
70 DCHECK(!usage.readable_files.empty());
71 if (usage.readable_files.size() == 1) {
72 *embedded_path = usage.readable_files.front();
73 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_SINGLE_READABLE_FILE_TEXT;
74 }
75 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_READABLE_FILES_TEXT;
76 }
77 }
78
79 // Only directories.
80 if (usage.writable_files.empty() && usage.readable_files.empty()) {
81 // Only writable directories.
82 if (usage.readable_directories.empty()) {
83 DCHECK(!usage.writable_directories.empty());
84 if (usage.writable_directories.size() == 1) {
85 *embedded_path = usage.writable_directories.front();
86 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_SINGLE_WRITABLE_DIRECTORY_TEXT;
87 }
88 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_WRITABLE_DIRECTORIES_TEXT;
89 }
90
91 // Only readable directories.
92 if (usage.writable_directories.empty()) {
93 DCHECK(!usage.readable_directories.empty());
94 if (usage.readable_directories.size() == 1) {
95 *embedded_path = usage.readable_directories.front();
96 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_SINGLE_READABLE_DIRECTORY_TEXT;
97 }
98 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_READABLE_DIRECTORIES_TEXT;
99 }
100 }
101
102 // Only readable files and directories.
103 if (usage.writable_files.empty() && usage.writable_directories.empty()) {
104 DCHECK(!usage.readable_files.empty());
105 DCHECK(!usage.readable_directories.empty());
106 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_READABLE_FILES_AND_DIRECTORIES_TEXT;
107 }
108
109 // Only writable files and directories.
110 if (usage.readable_files.empty() && usage.readable_directories.empty()) {
111 DCHECK(!usage.writable_files.empty());
112 DCHECK(!usage.writable_directories.empty());
113 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_WRITABLE_FILES_AND_DIRECTORIES_TEXT;
114 }
115
116 // Some combination of read and/or write access to files and/or directories.
117 return IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_READ_AND_WRITE;
118 }
119
120 // Displays a (one-column) table model as a one-line summary showing the
121 // first few items, with a toggle button to expand a table below to contain the
122 // full list of items.
123 class CollapsibleListView : public views::View {
124 public:
125 // How many rows to show in the expanded table without having to scroll.
126 static constexpr int kExpandedTableRowCount = 3;
127
CollapsibleListView(ui::TableModel * model)128 explicit CollapsibleListView(ui::TableModel* model) {
129 const views::LayoutProvider* provider = ChromeLayoutProvider::Get();
130
131 SetLayoutManager(std::make_unique<views::BoxLayout>(
132 views::BoxLayout::Orientation::kVertical, gfx::Insets(0, 0),
133 provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
134
135 auto label_container = std::make_unique<views::View>();
136 int indent =
137 provider->GetDistanceMetric(DISTANCE_SUBSECTION_HORIZONTAL_INDENT);
138 auto* label_layout =
139 label_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
140 views::BoxLayout::Orientation::kHorizontal,
141 gfx::Insets(/*vertical=*/0, indent),
142 provider->GetDistanceMetric(
143 views::DISTANCE_RELATED_LABEL_HORIZONTAL)));
144 base::string16 label_text;
145 if (model->RowCount() > 0) {
146 auto icon = std::make_unique<views::ImageView>();
147 icon->SetImage(model->GetIcon(0));
148 label_container->AddChildView(std::move(icon));
149
150 base::string16 first_item = model->GetText(0, 0);
151 base::string16 second_item =
152 model->RowCount() > 1 ? model->GetText(1, 0) : base::string16();
153
154 label_text = base::i18n::MessageFormatter::FormatWithNumberedArgs(
155 l10n_util::GetStringUTF16(
156 IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_FILES_TEXT),
157 model->RowCount(), first_item, second_item);
158 }
159 auto* label = label_container->AddChildView(std::make_unique<views::Label>(
160 label_text, CONTEXT_DIALOG_BODY_TEXT_SMALL,
161 views::style::STYLE_PRIMARY));
162 label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
163 label_layout->SetFlexForView(label, 1);
164 auto button = views::CreateVectorToggleImageButton(base::BindRepeating(
165 &CollapsibleListView::ButtonPressed, base::Unretained(this)));
166 button->SetTooltipText(
167 l10n_util::GetStringUTF16(IDS_NATIVE_FILE_SYSTEM_USAGE_EXPAND));
168 button->SetToggledTooltipText(
169 l10n_util::GetStringUTF16(IDS_NATIVE_FILE_SYSTEM_USAGE_COLLAPSE));
170 expand_collapse_button_ = label_container->AddChildView(std::move(button));
171 if (model->RowCount() < 3)
172 expand_collapse_button_->SetVisible(false);
173 int preferred_width = label_container->GetPreferredSize().width();
174 AddChildView(std::move(label_container));
175
176 std::vector<ui::TableColumn> table_columns{ui::TableColumn()};
177 auto table_view = std::make_unique<views::TableView>(
178 model, std::move(table_columns), views::ICON_AND_TEXT,
179 /*single_selection=*/true);
180 table_view->SetEnabled(false);
181 int row_height = table_view->GetRowHeight();
182 int table_height = table_view->GetPreferredSize().height();
183 table_view_parent_ = AddChildView(
184 views::TableView::CreateScrollViewWithTable(std::move(table_view)));
185 // Ideally we'd use table_view_parent_->GetInsets().height(), but that only
186 // returns the correct value after the view has been added to a root widget.
187 // So just hardcode the inset height to 2 pixels as that is what the scroll
188 // view uses.
189 int inset_height = 2;
190 table_view_parent_->SetPreferredSize(
191 gfx::Size(preferred_width,
192 std::min(table_height, kExpandedTableRowCount * row_height) +
193 inset_height));
194 table_view_parent_->SetVisible(false);
195 }
196
197 // views::View
OnThemeChanged()198 void OnThemeChanged() override {
199 views::View::OnThemeChanged();
200 auto* theme = GetNativeTheme();
201 const SkColor icon_color =
202 theme->GetSystemColor(ui::NativeTheme::kColorId_DefaultIconColor);
203 const SkColor disabled_icon_color =
204 theme->GetSystemColor(ui::NativeTheme::kColorId_DisabledIconColor);
205 views::SetImageFromVectorIconWithColor(
206 expand_collapse_button_, kCaretDownIcon, ui::TableModel::kIconSize,
207 icon_color);
208 views::SetToggledImageFromVectorIconWithColor(
209 expand_collapse_button_, kCaretUpIcon, ui::TableModel::kIconSize,
210 icon_color, disabled_icon_color);
211 }
212
213 private:
ButtonPressed()214 void ButtonPressed() {
215 table_is_expanded_ = !table_is_expanded_;
216 expand_collapse_button_->SetToggled(table_is_expanded_);
217 table_view_parent_->SetVisible(table_is_expanded_);
218 PreferredSizeChanged();
219 }
220
221 bool table_is_expanded_ = false;
222 views::ScrollView* table_view_parent_;
223 views::ToggleImageButton* expand_collapse_button_;
224 };
225
226 } // namespace
227
228 NativeFileSystemUsageBubbleView::Usage::Usage() = default;
229 NativeFileSystemUsageBubbleView::Usage::~Usage() = default;
230 NativeFileSystemUsageBubbleView::Usage::Usage(Usage&&) = default;
231 NativeFileSystemUsageBubbleView::Usage& NativeFileSystemUsageBubbleView::Usage::
232 operator=(Usage&&) = default;
233
FilePathListModel(const views::View * owner,std::vector<base::FilePath> files,std::vector<base::FilePath> directories)234 NativeFileSystemUsageBubbleView::FilePathListModel::FilePathListModel(
235 const views::View* owner,
236 std::vector<base::FilePath> files,
237 std::vector<base::FilePath> directories)
238 : owner_(owner),
239 files_(std::move(files)),
240 directories_(std::move(directories)) {}
241
242 NativeFileSystemUsageBubbleView::FilePathListModel::~FilePathListModel() =
243 default;
244
RowCount()245 int NativeFileSystemUsageBubbleView::FilePathListModel::RowCount() {
246 return files_.size() + directories_.size();
247 }
248
GetText(int row,int column_id)249 base::string16 NativeFileSystemUsageBubbleView::FilePathListModel::GetText(
250 int row,
251 int column_id) {
252 if (size_t{row} < files_.size())
253 return files_[row].BaseName().LossyDisplayName();
254 return directories_[row - files_.size()].BaseName().LossyDisplayName();
255 }
256
GetIcon(int row)257 gfx::ImageSkia NativeFileSystemUsageBubbleView::FilePathListModel::GetIcon(
258 int row) {
259 const SkColor icon_color = owner_->GetNativeTheme()->GetSystemColor(
260 ui::NativeTheme::kColorId_DefaultIconColor);
261 return gfx::CreateVectorIcon(size_t{row} < files_.size()
262 ? vector_icons::kInsertDriveFileOutlineIcon
263 : vector_icons::kFolderOpenIcon,
264 kIconSize, icon_color);
265 }
266
GetTooltip(int row)267 base::string16 NativeFileSystemUsageBubbleView::FilePathListModel::GetTooltip(
268 int row) {
269 if (size_t{row} < files_.size())
270 return files_[row].LossyDisplayName();
271 return directories_[row - files_.size()].LossyDisplayName();
272 }
273
SetObserver(ui::TableModelObserver *)274 void NativeFileSystemUsageBubbleView::FilePathListModel::SetObserver(
275 ui::TableModelObserver*) {}
276
277 // static
278 NativeFileSystemUsageBubbleView* NativeFileSystemUsageBubbleView::bubble_ =
279 nullptr;
280
281 // static
ShowBubble(content::WebContents * web_contents,const url::Origin & origin,Usage usage)282 void NativeFileSystemUsageBubbleView::ShowBubble(
283 content::WebContents* web_contents,
284 const url::Origin& origin,
285 Usage usage) {
286 base::RecordAction(
287 base::UserMetricsAction("NativeFileSystemAPI.OpenedBubble"));
288
289 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
290 if (!browser)
291 return;
292
293 ToolbarButtonProvider* button_provider =
294 BrowserView::GetBrowserViewForBrowser(browser)->toolbar_button_provider();
295
296 // Writable files or directories are generally also readable, but we don't
297 // want to display the same path twice. So filter out any writable paths from
298 // the readable lists.
299 std::set<base::FilePath> writable_directories(
300 usage.writable_directories.begin(), usage.writable_directories.end());
301 base::EraseIf(usage.readable_directories, [&](const base::FilePath& path) {
302 return base::Contains(writable_directories, path);
303 });
304 std::set<base::FilePath> writable_files(usage.writable_files.begin(),
305 usage.writable_files.end());
306 base::EraseIf(usage.readable_files, [&](const base::FilePath& path) {
307 return base::Contains(writable_files, path);
308 });
309
310 bubble_ = new NativeFileSystemUsageBubbleView(
311 button_provider->GetAnchorView(
312 PageActionIconType::kNativeFileSystemAccess),
313 web_contents, origin, std::move(usage));
314
315 bubble_->SetHighlightedButton(button_provider->GetPageActionIconView(
316 PageActionIconType::kNativeFileSystemAccess));
317 views::BubbleDialogDelegateView::CreateBubble(bubble_);
318
319 bubble_->ShowForReason(DisplayReason::USER_GESTURE,
320 /*allow_refocus_alert=*/true);
321 }
322
323 // static
CloseCurrentBubble()324 void NativeFileSystemUsageBubbleView::CloseCurrentBubble() {
325 if (bubble_)
326 bubble_->CloseBubble();
327 }
328
329 // static
GetBubble()330 NativeFileSystemUsageBubbleView* NativeFileSystemUsageBubbleView::GetBubble() {
331 return bubble_;
332 }
333
NativeFileSystemUsageBubbleView(views::View * anchor_view,content::WebContents * web_contents,const url::Origin & origin,Usage usage)334 NativeFileSystemUsageBubbleView::NativeFileSystemUsageBubbleView(
335 views::View* anchor_view,
336 content::WebContents* web_contents,
337 const url::Origin& origin,
338 Usage usage)
339 : LocationBarBubbleDelegateView(anchor_view, web_contents),
340 origin_(origin),
341 usage_(std::move(usage)),
342 readable_paths_model_(this,
343 std::move(usage_.readable_files),
344 std::move(usage_.readable_directories)),
345 writable_paths_model_(this,
346 std::move(usage_.writable_files),
347 std::move(usage_.writable_directories)) {
348 SetButtonLabel(ui::DIALOG_BUTTON_OK, l10n_util::GetStringUTF16(IDS_DONE));
349 SetButtonLabel(
350 ui::DIALOG_BUTTON_CANCEL,
351 l10n_util::GetStringUTF16(IDS_NATIVE_FILE_SYSTEM_USAGE_REMOVE_ACCESS));
352 SetCancelCallback(
353 base::BindOnce(&NativeFileSystemUsageBubbleView::OnDialogCancelled,
354 base::Unretained(this)));
355 set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
356 views::DISTANCE_BUBBLE_PREFERRED_WIDTH));
357 }
358
359 NativeFileSystemUsageBubbleView::~NativeFileSystemUsageBubbleView() = default;
360
GetAccessibleWindowTitle() const361 base::string16 NativeFileSystemUsageBubbleView::GetAccessibleWindowTitle()
362 const {
363 Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
364 // Don't crash if the web_contents is destroyed/unloaded.
365 if (!browser)
366 return {};
367
368 return BrowserView::GetBrowserViewForBrowser(browser)
369 ->toolbar_button_provider()
370 ->GetPageActionIconView(PageActionIconType::kNativeFileSystemAccess)
371 ->GetTextForTooltipAndAccessibleName();
372 }
373
ShouldShowCloseButton() const374 bool NativeFileSystemUsageBubbleView::ShouldShowCloseButton() const {
375 return true;
376 }
377
Init()378 void NativeFileSystemUsageBubbleView::Init() {
379 // Set up the layout of the bubble.
380 const views::LayoutProvider* provider = ChromeLayoutProvider::Get();
381 gfx::Insets dialog_insets =
382 provider->GetInsetsMetric(views::InsetsMetric::INSETS_DIALOG);
383 SetLayoutManager(std::make_unique<views::BoxLayout>(
384 views::BoxLayout::Orientation::kVertical,
385 gfx::Insets(0, dialog_insets.left(), 0, dialog_insets.right()),
386 provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
387 set_margins(
388 gfx::Insets(provider->GetDistanceMetric(
389 views::DISTANCE_DIALOG_CONTENT_MARGIN_TOP_TEXT),
390 0,
391 provider->GetDistanceMetric(
392 views::DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL),
393 0));
394
395 base::FilePath embedded_path;
396 int heading_message_id =
397 ComputeHeadingMessageFromUsage(usage_, &embedded_path);
398
399 if (!embedded_path.empty()) {
400 AddChildView(native_file_system_ui_helper::CreateOriginPathLabel(
401 heading_message_id, origin_, embedded_path,
402 views::style::CONTEXT_DIALOG_BODY_TEXT,
403 /*show_emphasis=*/false));
404 } else {
405 AddChildView(native_file_system_ui_helper::CreateOriginLabel(
406 heading_message_id, origin_, views::style::CONTEXT_DIALOG_BODY_TEXT,
407 /*show_emphasis=*/false));
408
409 if (writable_paths_model_.RowCount() > 0) {
410 if (readable_paths_model_.RowCount() > 0) {
411 auto label = std::make_unique<views::Label>(
412 l10n_util::GetStringUTF16(
413 IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_SAVE_CHANGES),
414 views::style::CONTEXT_DIALOG_BODY_TEXT,
415 views::style::STYLE_PRIMARY);
416 label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
417 AddChildView(std::move(label));
418 }
419 AddChildView(
420 std::make_unique<CollapsibleListView>(&writable_paths_model_));
421 }
422
423 if (readable_paths_model_.RowCount() > 0) {
424 if (writable_paths_model_.RowCount() > 0) {
425 auto label = std::make_unique<views::Label>(
426 l10n_util::GetStringUTF16(
427 IDS_NATIVE_FILE_SYSTEM_USAGE_BUBBLE_VIEW_CHANGES),
428 views::style::CONTEXT_DIALOG_BODY_TEXT,
429 views::style::STYLE_PRIMARY);
430 label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
431 AddChildView(std::move(label));
432 }
433 AddChildView(
434 std::make_unique<CollapsibleListView>(&readable_paths_model_));
435 }
436 }
437 }
438
OnDialogCancelled()439 void NativeFileSystemUsageBubbleView::OnDialogCancelled() {
440 base::RecordAction(
441 base::UserMetricsAction("NativeFileSystemAPI.RevokePermissions"));
442
443 if (!web_contents())
444 return;
445
446 content::BrowserContext* profile = web_contents()->GetBrowserContext();
447 auto* context =
448 NativeFileSystemPermissionContextFactory::GetForProfileIfExists(profile);
449 if (!context)
450 return;
451
452 context->RevokeGrants(origin_);
453 }
454
WindowClosing()455 void NativeFileSystemUsageBubbleView::WindowClosing() {
456 // |bubble_| can be a new bubble by this point (as Close(); doesn't
457 // call this right away). Only set to nullptr when it's this bubble.
458 if (bubble_ == this)
459 bubble_ = nullptr;
460 }
461
CloseBubble()462 void NativeFileSystemUsageBubbleView::CloseBubble() {
463 // Widget's Close() is async, but we don't want to use bubble_ after
464 // this. Additionally web_contents() may have been destroyed.
465 bubble_ = nullptr;
466 LocationBarBubbleDelegateView::CloseBubble();
467 }
468
ChildPreferredSizeChanged(views::View * child)469 void NativeFileSystemUsageBubbleView::ChildPreferredSizeChanged(
470 views::View* child) {
471 LocationBarBubbleDelegateView::ChildPreferredSizeChanged(child);
472 SizeToContents();
473 }
474