1// Copyright 2014 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 "ash/system/user/user_card_view.h"
6
7#include <algorithm>
8#include <vector>
9
10#include "ash/session/session_state_delegate.h"
11#include "ash/shell.h"
12#include "ash/system/tray/system_tray_delegate.h"
13#include "ash/system/tray/system_tray_notifier.h"
14#include "ash/system/tray/tray_constants.h"
15#include "ash/system/user/config.h"
16#include "ash/system/user/rounded_image_view.h"
17#include "base/i18n/rtl.h"
18#include "base/memory/scoped_vector.h"
19#include "base/strings/string16.h"
20#include "base/strings/string_util.h"
21#include "base/strings/utf_string_conversions.h"
22#include "components/user_manager/user_info.h"
23#include "grit/ash_resources.h"
24#include "grit/ash_strings.h"
25#include "ui/base/l10n/l10n_util.h"
26#include "ui/base/resource/resource_bundle.h"
27#include "ui/gfx/insets.h"
28#include "ui/gfx/range/range.h"
29#include "ui/gfx/rect.h"
30#include "ui/gfx/render_text.h"
31#include "ui/gfx/size.h"
32#include "ui/gfx/text_elider.h"
33#include "ui/gfx/text_utils.h"
34#include "ui/views/border.h"
35#include "ui/views/controls/link.h"
36#include "ui/views/controls/link_listener.h"
37#include "ui/views/layout/box_layout.h"
38
39#if defined(OS_CHROMEOS)
40#include "ash/ash_view_ids.h"
41#include "ash/media_delegate.h"
42#include "ash/system/tray/media_security/media_capture_observer.h"
43#include "ui/views/controls/image_view.h"
44#include "ui/views/layout/fill_layout.h"
45#endif
46
47namespace ash {
48namespace tray {
49
50namespace {
51
52const int kUserDetailsVerticalPadding = 5;
53
54// The invisible word joiner character, used as a marker to indicate the start
55// and end of the user's display name in the public account user card's text.
56const base::char16 kDisplayNameMark[] = {0x2060, 0};
57
58#if defined(OS_CHROMEOS)
59class MediaIndicator : public views::View, public MediaCaptureObserver {
60 public:
61  explicit MediaIndicator(MultiProfileIndex index)
62      : index_(index), label_(new views::Label) {
63    SetLayoutManager(new views::FillLayout);
64    views::ImageView* icon = new views::ImageView;
65    icon->SetImage(ui::ResourceBundle::GetSharedInstance()
66                       .GetImageNamed(IDR_AURA_UBER_TRAY_RECORDING_RED)
67                       .ToImageSkia());
68    AddChildView(icon);
69    label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
70    label_->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList(
71        ui::ResourceBundle::SmallFont));
72    OnMediaCaptureChanged();
73    Shell::GetInstance()->system_tray_notifier()->AddMediaCaptureObserver(this);
74    set_id(VIEW_ID_USER_VIEW_MEDIA_INDICATOR);
75  }
76
77  virtual ~MediaIndicator() {
78    Shell::GetInstance()->system_tray_notifier()->RemoveMediaCaptureObserver(
79        this);
80  }
81
82  // MediaCaptureObserver:
83  virtual void OnMediaCaptureChanged() OVERRIDE {
84    Shell* shell = Shell::GetInstance();
85    content::BrowserContext* context =
86        shell->session_state_delegate()->GetBrowserContextByIndex(index_);
87    MediaCaptureState state =
88        Shell::GetInstance()->media_delegate()->GetMediaCaptureState(context);
89    int res_id = 0;
90    switch (state) {
91      case MEDIA_CAPTURE_AUDIO_VIDEO:
92        res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO_VIDEO;
93        break;
94      case MEDIA_CAPTURE_AUDIO:
95        res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO;
96        break;
97      case MEDIA_CAPTURE_VIDEO:
98        res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_VIDEO;
99        break;
100      case MEDIA_CAPTURE_NONE:
101        break;
102    }
103    SetMessage(res_id ? l10n_util::GetStringUTF16(res_id) : base::string16());
104  }
105
106  views::View* GetMessageView() { return label_; }
107
108  void SetMessage(const base::string16& message) {
109    SetVisible(!message.empty());
110    label_->SetText(message);
111    label_->SetVisible(!message.empty());
112  }
113
114 private:
115  MultiProfileIndex index_;
116  views::Label* label_;
117
118  DISALLOW_COPY_AND_ASSIGN(MediaIndicator);
119};
120#endif
121
122// The user details shown in public account mode. This is essentially a label
123// but with custom painting code as the text is styled with multiple colors and
124// contains a link.
125class PublicAccountUserDetails : public views::View,
126                                 public views::LinkListener {
127 public:
128  PublicAccountUserDetails(int max_width);
129  virtual ~PublicAccountUserDetails();
130
131 private:
132  // Overridden from views::View.
133  virtual void Layout() OVERRIDE;
134  virtual gfx::Size GetPreferredSize() const OVERRIDE;
135  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
136
137  // Overridden from views::LinkListener.
138  virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
139
140  // Calculate a preferred size that ensures the label text and the following
141  // link do not wrap over more than three lines in total for aesthetic reasons
142  // if possible.
143  void CalculatePreferredSize(int max_allowed_width);
144
145  base::string16 text_;
146  views::Link* learn_more_;
147  gfx::Size preferred_size_;
148  ScopedVector<gfx::RenderText> lines_;
149
150  DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails);
151};
152
153PublicAccountUserDetails::PublicAccountUserDetails(int max_width)
154    : learn_more_(NULL) {
155  const int inner_padding =
156      kTrayPopupPaddingHorizontal - kTrayPopupPaddingBetweenItems;
157  const bool rtl = base::i18n::IsRTL();
158  SetBorder(views::Border::CreateEmptyBorder(kUserDetailsVerticalPadding,
159                                             rtl ? 0 : inner_padding,
160                                             kUserDetailsVerticalPadding,
161                                             rtl ? inner_padding : 0));
162
163  // Retrieve the user's display name and wrap it with markers.
164  // Note that since this is a public account it always has to be the primary
165  // user.
166  base::string16 display_name = Shell::GetInstance()
167                                    ->session_state_delegate()
168                                    ->GetUserInfo(0)
169                                    ->GetDisplayName();
170  base::RemoveChars(display_name, kDisplayNameMark, &display_name);
171  display_name = kDisplayNameMark[0] + display_name + kDisplayNameMark[0];
172  // Retrieve the domain managing the device and wrap it with markers.
173  base::string16 domain = base::UTF8ToUTF16(
174      Shell::GetInstance()->system_tray_delegate()->GetEnterpriseDomain());
175  base::RemoveChars(domain, kDisplayNameMark, &domain);
176  base::i18n::WrapStringWithLTRFormatting(&domain);
177  // Retrieve the label text, inserting the display name and domain.
178  text_ = l10n_util::GetStringFUTF16(
179      IDS_ASH_STATUS_TRAY_PUBLIC_LABEL, display_name, domain);
180
181  learn_more_ = new views::Link(l10n_util::GetStringUTF16(IDS_ASH_LEARN_MORE));
182  learn_more_->SetUnderline(false);
183  learn_more_->set_listener(this);
184  AddChildView(learn_more_);
185
186  CalculatePreferredSize(max_width);
187}
188
189PublicAccountUserDetails::~PublicAccountUserDetails() {}
190
191void PublicAccountUserDetails::Layout() {
192  lines_.clear();
193  const gfx::Rect contents_area = GetContentsBounds();
194  if (contents_area.IsEmpty())
195    return;
196
197  // Word-wrap the label text.
198  const gfx::FontList font_list;
199  std::vector<base::string16> lines;
200  gfx::ElideRectangleText(text_,
201                          font_list,
202                          contents_area.width(),
203                          contents_area.height(),
204                          gfx::ELIDE_LONG_WORDS,
205                          &lines);
206  // Loop through the lines, creating a renderer for each.
207  gfx::Point position = contents_area.origin();
208  gfx::Range display_name(gfx::Range::InvalidRange());
209  for (std::vector<base::string16>::const_iterator it = lines.begin();
210       it != lines.end();
211       ++it) {
212    gfx::RenderText* line = gfx::RenderText::CreateInstance();
213    line->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI);
214    line->SetText(*it);
215    const gfx::Size size(contents_area.width(), line->GetStringSize().height());
216    line->SetDisplayRect(gfx::Rect(position, size));
217    position.set_y(position.y() + size.height());
218
219    // Set the default text color for the line.
220    line->SetColor(kPublicAccountUserCardTextColor);
221
222    // If a range of the line contains the user's display name, apply a custom
223    // text color to it.
224    if (display_name.is_empty())
225      display_name.set_start(it->find(kDisplayNameMark));
226    if (!display_name.is_empty()) {
227      display_name.set_end(
228          it->find(kDisplayNameMark, display_name.start() + 1));
229      gfx::Range line_range(0, it->size());
230      line->ApplyColor(kPublicAccountUserCardNameColor,
231                       display_name.Intersect(line_range));
232      // Update the range for the next line.
233      if (display_name.end() >= line_range.end())
234        display_name.set_start(0);
235      else
236        display_name = gfx::Range::InvalidRange();
237    }
238
239    lines_.push_back(line);
240  }
241
242  // Position the link after the label text, separated by a space. If it does
243  // not fit onto the last line of the text, wrap the link onto its own line.
244  const gfx::Size last_line_size = lines_.back()->GetStringSize();
245  const int space_width =
246      gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list);
247  const gfx::Size link_size = learn_more_->GetPreferredSize();
248  if (contents_area.width() - last_line_size.width() >=
249      space_width + link_size.width()) {
250    position.set_x(position.x() + last_line_size.width() + space_width);
251    position.set_y(position.y() - last_line_size.height());
252  }
253  position.set_y(position.y() - learn_more_->GetInsets().top());
254  gfx::Rect learn_more_bounds(position, link_size);
255  learn_more_bounds.Intersect(contents_area);
256  if (base::i18n::IsRTL()) {
257    const gfx::Insets insets = GetInsets();
258    learn_more_bounds.Offset(insets.right() - insets.left(), 0);
259  }
260  learn_more_->SetBoundsRect(learn_more_bounds);
261}
262
263gfx::Size PublicAccountUserDetails::GetPreferredSize() const {
264  return preferred_size_;
265}
266
267void PublicAccountUserDetails::OnPaint(gfx::Canvas* canvas) {
268  for (ScopedVector<gfx::RenderText>::const_iterator it = lines_.begin();
269       it != lines_.end();
270       ++it) {
271    (*it)->Draw(canvas);
272  }
273  views::View::OnPaint(canvas);
274}
275
276void PublicAccountUserDetails::LinkClicked(views::Link* source,
277                                           int event_flags) {
278  DCHECK_EQ(source, learn_more_);
279  Shell::GetInstance()->system_tray_delegate()->ShowPublicAccountInfo();
280}
281
282void PublicAccountUserDetails::CalculatePreferredSize(int max_allowed_width) {
283  const gfx::FontList font_list;
284  const gfx::Size link_size = learn_more_->GetPreferredSize();
285  const int space_width =
286      gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list);
287  const gfx::Insets insets = GetInsets();
288  int min_width = link_size.width();
289  int max_width = std::min(
290      gfx::GetStringWidth(text_, font_list) + space_width + link_size.width(),
291      max_allowed_width - insets.width());
292  // Do a binary search for the minimum width that ensures no more than three
293  // lines are needed. The lower bound is the minimum of the current bubble
294  // width and the width of the link (as no wrapping is permitted inside the
295  // link). The upper bound is the maximum of the largest allowed bubble width
296  // and the sum of the label text and link widths when put on a single line.
297  std::vector<base::string16> lines;
298  while (min_width < max_width) {
299    lines.clear();
300    const int width = (min_width + max_width) / 2;
301    const bool too_narrow = gfx::ElideRectangleText(text_,
302                                                    font_list,
303                                                    width,
304                                                    INT_MAX,
305                                                    gfx::TRUNCATE_LONG_WORDS,
306                                                    &lines) != 0;
307    int line_count = lines.size();
308    if (!too_narrow && line_count == 3 &&
309        width - gfx::GetStringWidth(lines.back(), font_list) <=
310            space_width + link_size.width())
311      ++line_count;
312    if (too_narrow || line_count > 3)
313      min_width = width + 1;
314    else
315      max_width = width;
316  }
317
318  // Calculate the corresponding height and set the preferred size.
319  lines.clear();
320  gfx::ElideRectangleText(
321      text_, font_list, min_width, INT_MAX, gfx::TRUNCATE_LONG_WORDS, &lines);
322  int line_count = lines.size();
323  if (min_width - gfx::GetStringWidth(lines.back(), font_list) <=
324      space_width + link_size.width()) {
325    ++line_count;
326  }
327  const int line_height = font_list.GetHeight();
328  const int link_extra_height = std::max(
329      link_size.height() - learn_more_->GetInsets().top() - line_height, 0);
330  preferred_size_ =
331      gfx::Size(min_width + insets.width(),
332                line_count * line_height + link_extra_height + insets.height());
333}
334
335}  // namespace
336
337UserCardView::UserCardView(user::LoginStatus login_status,
338                           int max_width,
339                           int multiprofile_index) {
340  SetLayoutManager(new views::BoxLayout(
341      views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
342  switch (login_status) {
343    case user::LOGGED_IN_RETAIL_MODE:
344      AddRetailModeUserContent();
345      break;
346    case user::LOGGED_IN_PUBLIC:
347      AddPublicModeUserContent(max_width);
348      break;
349    default:
350      AddUserContent(login_status, multiprofile_index);
351      break;
352  }
353}
354
355UserCardView::~UserCardView() {}
356
357void UserCardView::AddRetailModeUserContent() {
358  views::Label* details = new views::Label;
359  details->SetText(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_KIOSK_LABEL));
360  details->SetBorder(views::Border::CreateEmptyBorder(0, 4, 0, 1));
361  details->SetHorizontalAlignment(gfx::ALIGN_LEFT);
362  AddChildView(details);
363}
364
365void UserCardView::AddPublicModeUserContent(int max_width) {
366  views::View* icon = CreateIcon(user::LOGGED_IN_PUBLIC, 0);
367  AddChildView(icon);
368  int details_max_width = max_width - icon->GetPreferredSize().width() -
369                          kTrayPopupPaddingBetweenItems;
370  AddChildView(new PublicAccountUserDetails(details_max_width));
371}
372
373void UserCardView::AddUserContent(user::LoginStatus login_status,
374                                  int multiprofile_index) {
375  views::View* icon = CreateIcon(login_status, multiprofile_index);
376  AddChildView(icon);
377  views::Label* user_name = NULL;
378  SessionStateDelegate* delegate =
379      Shell::GetInstance()->session_state_delegate();
380  if (!multiprofile_index) {
381    base::string16 user_name_string =
382        login_status == user::LOGGED_IN_GUEST
383            ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_GUEST_LABEL)
384            : delegate->GetUserInfo(multiprofile_index)->GetDisplayName();
385    if (user_name_string.empty() && IsMultiAccountSupportedAndUserActive())
386      user_name_string = base::ASCIIToUTF16(
387          delegate->GetUserInfo(multiprofile_index)->GetEmail());
388    if (!user_name_string.empty()) {
389      user_name = new views::Label(user_name_string);
390      user_name->SetHorizontalAlignment(gfx::ALIGN_LEFT);
391    }
392  }
393
394  views::Label* user_email = NULL;
395  if (login_status != user::LOGGED_IN_GUEST &&
396      (multiprofile_index || !IsMultiAccountSupportedAndUserActive())) {
397    SystemTrayDelegate* tray_delegate =
398        Shell::GetInstance()->system_tray_delegate();
399    base::string16 user_email_string =
400        tray_delegate->IsUserSupervised()
401            ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SUPERVISED_LABEL)
402            : base::UTF8ToUTF16(
403                  delegate->GetUserInfo(multiprofile_index)->GetEmail());
404    if (!user_email_string.empty()) {
405      user_email = new views::Label(user_email_string);
406      user_email->SetFontList(
407          ui::ResourceBundle::GetSharedInstance().GetFontList(
408              ui::ResourceBundle::SmallFont));
409      user_email->SetHorizontalAlignment(gfx::ALIGN_LEFT);
410    }
411  }
412
413  // Adjust text properties dependent on if it is an active or inactive user.
414  if (multiprofile_index) {
415    // Fade the text of non active users to 50%.
416    SkColor text_color = user_email->enabled_color();
417    text_color = SkColorSetA(text_color, SkColorGetA(text_color) / 2);
418    if (user_email)
419      user_email->SetDisabledColor(text_color);
420    if (user_name)
421      user_name->SetDisabledColor(text_color);
422  }
423
424  if (user_email && user_name) {
425    views::View* details = new views::View;
426    details->SetLayoutManager(new views::BoxLayout(
427        views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
428    details->AddChildView(user_name);
429    details->AddChildView(user_email);
430    AddChildView(details);
431  } else {
432    if (user_name)
433      AddChildView(user_name);
434    if (user_email) {
435#if defined(OS_CHROMEOS)
436      // Only non active user can have a media indicator.
437      MediaIndicator* media_indicator = new MediaIndicator(multiprofile_index);
438      views::View* email_indicator_view = new views::View;
439      email_indicator_view->SetLayoutManager(new views::BoxLayout(
440          views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
441      email_indicator_view->AddChildView(user_email);
442      email_indicator_view->AddChildView(media_indicator);
443
444      views::View* details = new views::View;
445      details->SetLayoutManager(new views::BoxLayout(
446          views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
447      details->AddChildView(email_indicator_view);
448      details->AddChildView(media_indicator->GetMessageView());
449      AddChildView(details);
450#else
451      AddChildView(user_email);
452#endif
453    }
454  }
455}
456
457views::View* UserCardView::CreateIcon(user::LoginStatus login_status,
458                                      int multiprofile_index) {
459  RoundedImageView* icon =
460      new RoundedImageView(kTrayAvatarCornerRadius, multiprofile_index == 0);
461  if (login_status == user::LOGGED_IN_GUEST) {
462    icon->SetImage(*ui::ResourceBundle::GetSharedInstance()
463                        .GetImageNamed(IDR_AURA_UBER_TRAY_GUEST_ICON)
464                        .ToImageSkia(),
465                   gfx::Size(kTrayAvatarSize, kTrayAvatarSize));
466  } else {
467    SessionStateDelegate* delegate =
468        Shell::GetInstance()->session_state_delegate();
469    content::BrowserContext* context =
470        delegate->GetBrowserContextByIndex(multiprofile_index);
471    icon->SetImage(delegate->GetUserInfo(context)->GetImage(),
472                   gfx::Size(kTrayAvatarSize, kTrayAvatarSize));
473  }
474  return icon;
475}
476
477}  // namespace tray
478}  // namespace ash
479