Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(11)

Side by Side Diff: ash/system/user/tray_user.cc

Issue 11377133: Customize user details in ash system bubble for public account mode (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "ash/system/user/tray_user.h" 5 #include "ash/system/user/tray_user.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <climits> 8 #include <climits>
9 #include <vector> 9 #include <vector>
10 10
11 #include "ash/shell.h" 11 #include "ash/shell.h"
12 #include "ash/system/tray/system_tray_delegate.h" 12 #include "ash/system/tray/system_tray_delegate.h"
13 #include "ash/system/tray/tray_constants.h" 13 #include "ash/system/tray/tray_constants.h"
14 #include "ash/system/tray/tray_item_view.h" 14 #include "ash/system/tray/tray_item_view.h"
15 #include "ash/system/tray/tray_views.h" 15 #include "ash/system/tray/tray_views.h"
16 #include "base/i18n/rtl.h"
17 #include "base/logging.h"
18 #include "base/memory/scoped_vector.h"
16 #include "base/string16.h" 19 #include "base/string16.h"
17 #include "base/utf_string_conversions.h" 20 #include "base/utf_string_conversions.h"
18 #include "grit/ash_strings.h" 21 #include "grit/ash_strings.h"
19 #include "skia/ext/image_operations.h" 22 #include "skia/ext/image_operations.h"
20 #include "third_party/skia/include/core/SkCanvas.h" 23 #include "third_party/skia/include/core/SkCanvas.h"
24 #include "third_party/skia/include/core/SkColor.h"
21 #include "third_party/skia/include/core/SkPaint.h" 25 #include "third_party/skia/include/core/SkPaint.h"
22 #include "third_party/skia/include/core/SkPath.h" 26 #include "third_party/skia/include/core/SkPath.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/base/native_theme/native_theme.h"
23 #include "ui/base/resource/resource_bundle.h" 29 #include "ui/base/resource/resource_bundle.h"
24 #include "ui/base/text/text_elider.h" 30 #include "ui/base/text/text_elider.h"
25 #include "ui/gfx/canvas.h" 31 #include "ui/gfx/canvas.h"
32 #include "ui/gfx/font.h"
26 #include "ui/gfx/image/image.h" 33 #include "ui/gfx/image/image.h"
27 #include "ui/gfx/image/image_skia_operations.h" 34 #include "ui/gfx/image/image_skia_operations.h"
28 #include "ui/gfx/insets.h" 35 #include "ui/gfx/insets.h"
36 #include "ui/gfx/rect.h"
29 #include "ui/gfx/size.h" 37 #include "ui/gfx/size.h"
30 #include "ui/gfx/skia_util.h" 38 #include "ui/gfx/skia_util.h"
31 #include "ui/views/border.h" 39 #include "ui/views/border.h"
32 #include "ui/views/controls/button/button.h" 40 #include "ui/views/controls/button/button.h"
33 #include "ui/views/controls/button/text_button.h" 41 #include "ui/views/controls/button/text_button.h"
34 #include "ui/views/controls/image_view.h" 42 #include "ui/views/controls/image_view.h"
35 #include "ui/views/controls/label.h" 43 #include "ui/views/controls/label.h"
44 #include "ui/views/controls/link.h"
45 #include "ui/views/controls/link_listener.h"
36 #include "ui/views/layout/box_layout.h" 46 #include "ui/views/layout/box_layout.h"
37 #include "ui/views/view.h" 47 #include "ui/views/view.h"
48 #include "ui/views/view_text_utils.h"
38 #include "ui/views/widget/widget.h" 49 #include "ui/views/widget/widget.h"
39 50
40 namespace { 51 namespace {
41 52
42 const int kUserInfoVerticalPadding = 10; 53 const int kUserDetailsVerticalPadding = 5;
54 const int kUserCardVerticalPadding = 10;
55 const int kProfileRoundedCornerRadius = 2;
43 const int kUserIconSize = 27; 56 const int kUserIconSize = 27;
44 const int kProfileRoundedCornerRadius = 2; 57
58 string16 kDisplayName() {
59 return ASCIIToUTF16("DISPLAY_NAME");
60 }
61
62 string16 kDomain() {
63 return ASCIIToUTF16("DOMAIN");
64 }
45 65
46 } // namespace 66 } // namespace
47 67
48 namespace ash { 68 namespace ash {
49 namespace internal { 69 namespace internal {
50 70
51 namespace tray { 71 namespace tray {
52 72
53 // A custom image view with rounded edges. 73 // A custom image view with rounded edges.
54 class RoundedImageView : public views::View { 74 class RoundedImageView : public views::View {
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
99 119
100 private: 120 private:
101 gfx::ImageSkia image_; 121 gfx::ImageSkia image_;
102 gfx::ImageSkia resized_; 122 gfx::ImageSkia resized_;
103 gfx::Size image_size_; 123 gfx::Size image_size_;
104 int corner_radius_; 124 int corner_radius_;
105 125
106 DISALLOW_COPY_AND_ASSIGN(RoundedImageView); 126 DISALLOW_COPY_AND_ASSIGN(RoundedImageView);
107 }; 127 };
108 128
129 // A chunk of text with associated color.
130 class TextChunk {
131 public:
132 TextChunk(const string16& text, const SkColor& color, bool is_url)
133 : text_(text), color_(color), is_url_(is_url) {}
134
135 const string16& Text() const { return text_; }
136 const SkColor& Color() const { return color_; }
137 bool IsUrl() const { return is_url_; }
138
139 private:
140 string16 text_;
141 SkColor color_;
142 bool is_url_;
143
144 DISALLOW_COPY_AND_ASSIGN(TextChunk);
145 };
146
147 // The user details shown in public account mode. This is essentially a label
148 // but with custom painting code as the text is styled with multiple colors and
149 // contains a link.
150 class PublicAccountUserDetails : public views::View,
151 public views::LinkListener {
152 public:
153 PublicAccountUserDetails() : learn_more_(NULL) {
154 set_border(views::Border::CreateEmptyBorder(
155 kUserDetailsVerticalPadding, 0, kUserDetailsVerticalPadding, 0));
156 const string16 text =
157 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_PUBLIC_LABEL);
158 const size_t display_name_start = text.find(kDisplayName());
159 DCHECK(display_name_start != string16::npos);
160 const size_t domain_start = text.find(kDomain());
161 DCHECK(domain_start != string16::npos);
162 const size_t display_name_end = display_name_start +
163 kDisplayName().size();
164 const size_t domain_end = domain_start + kDomain().size();
165 bool display_name_first = display_name_start < domain_start;
166
167 SkColor default_color = GetNativeTheme()->GetSystemColor(
168 ui::NativeTheme::kColorId_LabelEnabledColor);
169 ash::SystemTrayDelegate* tray = ash::Shell::GetInstance()->tray_delegate();
170 TextChunk* display_name = new TextChunk(tray->GetUserDisplayName(),
171 default_color, false);
172 TextChunk *domain = new TextChunk(UTF8ToUTF16(tray->GetEnterpriseDomain()),
173 kPublicAccountUserCardTextColor, true);
174 const size_t chunk_1_end = std::min(display_name_start, domain_start);
175 chunks_.push_back(new TextChunk(text.substr(0, chunk_1_end),
176 kPublicAccountUserCardTextColor, false));
177 chunks_.push_back(display_name_first ? display_name : domain);
178 const size_t chunk_2_start = std::min(display_name_end, domain_end);
179 const size_t chunk_2_end = std::max(display_name_start, domain_start);
180 chunks_.push_back(new TextChunk(text.substr(chunk_2_start,
181 chunk_2_end - chunk_2_start),
182 kPublicAccountUserCardTextColor, false));
183 chunks_.push_back(display_name_first ? domain : display_name);
184 const size_t chunk_3_start = std::max(display_name_end, domain_end);
185 chunks_.push_back(new TextChunk(text.substr(chunk_3_start),
186 kPublicAccountUserCardTextColor, false));
stevenjb 2012/11/13 20:54:22 This seems like something that ought to be general
bartfab (slow) 2012/11/16 19:59:15 Done.
187 chunks_.push_back(new TextChunk(ASCIIToUTF16(" "),
188 kPublicAccountUserCardTextColor, false));
stevenjb 2012/11/13 20:54:22 This bit seems strange to me. Is this the spacing
bartfab (slow) 2012/11/16 19:59:15 Done.
189
190 learn_more_ = new views::Link(
191 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_PUBLIC_LEARN_MORE));
192 learn_more_->set_listener(this);
193 AddChildView(learn_more_);
194 }
195
196 // Overridden from views::LinkListener.
197 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE {
198 DCHECK_EQ(source, learn_more_);
199 ash::Shell::GetInstance()->tray_delegate()->ShowPublicAccountInfo();
200 }
201
202 // Overridden from views::View.
203 void Layout() OVERRIDE {
204 learn_more_->SetBoundsRect(PaintTextAndCalculateLinkRect(
205 NULL, GetContentsBounds()));
206 }
207
208 virtual gfx::Size GetPreferredSize() OVERRIDE {
209 if (!visible())
210 return gfx::Size();
211 const gfx::Insets insets = GetInsets();
212 const gfx::Rect link_rect = PaintTextAndCalculateLinkRect(
213 NULL, gfx::Rect(INT_MAX, INT_MAX));
214 return gfx::Size(link_rect.right() + insets.width(),
215 link_rect.bottom() + insets.height());
216 }
217
218 virtual int GetHeightForWidth(int w) OVERRIDE {
219 if (!visible())
220 return 0;
221 const gfx::Insets insets = GetInsets();
222 const gfx::Rect link_rect = PaintTextAndCalculateLinkRect(
223 NULL, gfx::Rect(w - insets.width(), INT_MAX));
224 return link_rect.bottom() + GetInsets().height();
225 }
226
227 private:
228 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
229 views::View::OnPaint(canvas);
230 PaintTextAndCalculateLinkRect(canvas, GetContentsBounds());
231 }
232
233 gfx::Rect PaintTextAndCalculateLinkRect(gfx::Canvas* canvas,
234 const gfx::Rect& content_area) {
235 gfx::Size position;
236 const bool text_direction_is_rtl = base::i18n::IsRTL();
237 const gfx::Font& font = label_.font();
238 label_.SetBoundsRect(content_area);
239 for (ScopedVector<TextChunk>::const_iterator it = chunks_.begin();
240 it != chunks_.end(); ++it) {
241 label_.SetEnabledColor((*it)->Color());
242 string16 text;
243 if ((*it)->IsUrl()) {
244 text = ui::ElideText((*it)->Text(), font, content_area.width(),
245 ui::ELIDE_IN_MIDDLE);
246 base::i18n::WrapStringWithLTRFormatting(&text);
247 } else {
248 text = (*it)->Text();
249 }
250 view_text_utils::DrawTextAndPositionUrl(
251 canvas, &label_, text, NULL, NULL, &position, text_direction_is_rtl,
252 content_area, font);
253 }
stevenjb 2012/11/13 20:54:22 So, I don't want to overcomplicate this CL too muc
bartfab (slow) 2012/11/16 19:59:15 I went for the bonus points: I replaced the views_
254 gfx::Rect link_rect;
255 view_text_utils::DrawTextAndPositionUrl(
256 canvas, &label_, string16(), learn_more_, &link_rect, &position,
257 text_direction_is_rtl, content_area, font);
258 // The link is separated from the text by a single space. If the link was
259 // wrapped onto a new line, remove the preceding space as it is not needed
260 // in this case.
261 const int space_width =
262 gfx::Canvas::GetStringWidth(ASCIIToUTF16(" "), font);
263 if (link_rect.x() == content_area.x() + space_width)
264 link_rect.Offset(-space_width, 0);
265 return link_rect;
266 }
267
268 ScopedVector<TextChunk> chunks_;
269 views::Label label_;
270 views::Link* learn_more_;
271
272 DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails);
273 };
274
275 // The user card, consisting of an avatar picture (except in retail mode) and
276 // user details.
277 class UserCard : public views::View {
278 public:
279 explicit UserCard(ash::user::LoginStatus login)
280 : avatar_(NULL), details_(NULL) {
281 set_border(views::Border::CreateEmptyBorder(kUserCardVerticalPadding, 0,
282 kUserCardVerticalPadding, 0));
283 if (login == ash::user::LOGGED_IN_KIOSK) {
284 views::Label* details = new views::Label;
285 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
286 details->SetText(
287 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_KIOSK_LABEL));
288 details->set_border(views::Border::CreateEmptyBorder(0, 4, 0, 1));
289 details->SetHorizontalAlignment(gfx::ALIGN_LEFT);
290 AddChildView(details);
291 details_ = details;
292 return;
293 }
294
295 avatar_ = new RoundedImageView(kProfileRoundedCornerRadius);
296 avatar_->SetImage(
297 ash::Shell::GetInstance()->tray_delegate()->GetUserImage(),
298 gfx::Size(kUserIconSize, kUserIconSize));
299 AddChildView(avatar_);
300
301 if (login == ash::user::LOGGED_IN_PUBLIC) {
302 details_ = new PublicAccountUserDetails();
303 AddChildView(details_);
304 return;
305 }
306
307 ash::SystemTrayDelegate* tray = ash::Shell::GetInstance()->tray_delegate();
308 views::View* details = new views::View;
309 details->SetLayoutManager(new views::BoxLayout(
310 views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
311 views::Label* username = new views::Label(tray->GetUserDisplayName());
312 username->SetHorizontalAlignment(gfx::ALIGN_LEFT);
313 details->AddChildView(username);
314
315 views::Label* email = new views::Label(UTF8ToUTF16(tray->GetUserEmail()));
316 email->SetFont(username->font().DeriveFont(-1));
317 email->SetHorizontalAlignment(gfx::ALIGN_LEFT);
318 email->SetEnabled(false);
319 details->AddChildView(email);
320 AddChildView(details);
321 details_ = details;
322 }
323
324 // Overridden from views::View.
325 virtual gfx::Size GetPreferredSize() OVERRIDE {
326 gfx::Size size;
327 if (!visible())
328 return size;
329 if (avatar_)
330 size = avatar_->GetPreferredSize();
331 if (details_) {
332 gfx::Size details_size = details_->GetPreferredSize();
333 size.set_height(std::max(size.height(), details_size.height()));
334 size.Enlarge(details_size.width(), 0);
335 }
336 if (avatar_ && details_)
337 size.Enlarge(kTrayPopupPaddingBetweenItems, 0);
338 gfx::Insets insets = GetInsets();
339 size.Enlarge(insets.width(), insets.height());
340 return size;
341 }
342
343 virtual int GetHeightForWidth(int w) OVERRIDE {
344 if (!visible())
345 return 0;
346 gfx::Insets insets = GetInsets();
347 int width = w - insets.width();
348 int height = 0;
349 if (avatar_) {
350 gfx::Size avatar_size = avatar_->GetPreferredSize();
351 width -= avatar_size.width();
352 height = avatar_size.height();
353 }
354 if (avatar_ && details_)
355 width -= kTrayPopupPaddingBetweenItems;
356 if (details_)
357 height = std::max(height, details_->GetHeightForWidth(width));
358 return height + insets.height();
359 }
360
361 virtual void Layout() OVERRIDE {
362 gfx::Rect contents_area(GetContentsBounds());
363 gfx::Point top_left = contents_area.origin();
364 if (avatar_) {
365 const int avatar_width = avatar_->GetPreferredSize().width();
366 gfx::Rect avatar_area = contents_area;
367 avatar_area.set_width(avatar_width);
368 avatar_->SetBoundsRect(avatar_area);
369 contents_area.Inset(avatar_width, 0, 0, 0);
370 }
371 if (avatar_ && details_)
372 contents_area.Inset(kTrayPopupPaddingBetweenItems, 0, 0, 0);
373 if (details_)
374 details_->SetBoundsRect(contents_area);
375 }
376
377 private:
378 RoundedImageView* avatar_;
379 views::View* details_;
380
381 DISALLOW_COPY_AND_ASSIGN(UserCard);
382 };
383
109 // A custom tray popup text button that can be queried for its preferred width 384 // A custom tray popup text button that can be queried for its preferred width
110 // with a given upper limit, allowing the button's width to be reduced when 385 // with a given upper limit, allowing the button's width to be reduced when
111 // space is tight while guaranteeing that its content is wrapped and displayed 386 // space is tight while guaranteeing that its content is wrapped and displayed
112 // without any elision. 387 // without any elision.
113 class PopupLogoutButton : public TrayPopupTextButton { 388 class PopupLogoutButton : public TrayPopupTextButton {
114 public: 389 public:
115 PopupLogoutButton(views::ButtonListener* listener, const string16& text) 390 PopupLogoutButton(views::ButtonListener* listener, const string16& text)
116 : TrayPopupTextButton(listener, text) { 391 : TrayPopupTextButton(listener, text) {
117 SetMultiLine(true); 392 SetMultiLine(true);
118 } 393 }
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
174 449
175 class UserView : public views::View, 450 class UserView : public views::View,
176 public views::ButtonListener { 451 public views::ButtonListener {
177 public: 452 public:
178 explicit UserView(ash::user::LoginStatus login) : user_card_(NULL), 453 explicit UserView(ash::user::LoginStatus login) : user_card_(NULL),
179 logout_button_(NULL) { 454 logout_button_(NULL) {
180 CHECK(login != ash::user::LOGGED_IN_NONE); 455 CHECK(login != ash::user::LOGGED_IN_NONE);
181 set_background(views::Background::CreateSolidBackground( 456 set_background(views::Background::CreateSolidBackground(
182 login == ash::user::LOGGED_IN_PUBLIC ? kPublicAccountBackgroundColor : 457 login == ash::user::LOGGED_IN_PUBLIC ? kPublicAccountBackgroundColor :
183 kBackgroundColor)); 458 kBackgroundColor));
184 AddUserInfo(login); 459 AddUserCard(login);
185 AddLogoutButton(login); 460 AddLogoutButton(login);
186 } 461 }
187 462
188 // Overridden from views::View. 463 // Overridden from views::View.
189 virtual gfx::Size GetPreferredSize() OVERRIDE { 464 virtual gfx::Size GetPreferredSize() OVERRIDE {
190 gfx::Size size; 465 gfx::Size size;
191 if (!visible()) 466 if (!visible())
192 return size; 467 return size;
193 if (user_card_) 468 if (user_card_)
194 size = user_card_->GetPreferredSize(); 469 size = user_card_->GetPreferredSize();
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 (logout_area.width() + kTrayPopupPaddingBetweenItems)); 525 (logout_area.width() + kTrayPopupPaddingBetweenItems));
251 user_card_->SetBoundsRect(user_card_area); 526 user_card_->SetBoundsRect(user_card_area);
252 } else if (user_card_) { 527 } else if (user_card_) {
253 user_card_->SetBoundsRect(contents_area); 528 user_card_->SetBoundsRect(contents_area);
254 } else if (logout_button_) { 529 } else if (logout_button_) {
255 logout_button_->SetBoundsRect(contents_area); 530 logout_button_->SetBoundsRect(contents_area);
256 } 531 }
257 } 532 }
258 533
259 private: 534 private:
260 void AddUserInfo(ash::user::LoginStatus login) { 535 void AddUserCard(ash::user::LoginStatus login) {
261 if (login == ash::user::LOGGED_IN_GUEST) 536 if (login == ash::user::LOGGED_IN_GUEST)
262 return; 537 return;
263 538
264 set_border(views::Border::CreateEmptyBorder( 539 set_border(views::Border::CreateEmptyBorder(
265 0, kTrayPopupPaddingHorizontal, 0, kTrayPopupPaddingHorizontal)); 540 0, kTrayPopupPaddingHorizontal, 0, kTrayPopupPaddingHorizontal));
266 541
267 user_card_ = new views::View; 542 user_card_ = new UserCard(login);
268 user_card_->SetLayoutManager(new views::BoxLayout(
269 views::BoxLayout::kHorizontal, 0,
270 kUserInfoVerticalPadding, kTrayPopupPaddingBetweenItems));
271 AddChildView(user_card_); 543 AddChildView(user_card_);
272 544 return;
273 if (login == ash::user::LOGGED_IN_KIOSK) {
274 views::Label* label = new views::Label;
275 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
276 label->SetText(
277 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_KIOSK_LABEL));
278 label->set_border(views::Border::CreateEmptyBorder(
279 0, 4, 0, 1));
280 label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
281 user_card_->AddChildView(label);
282 return;
283 }
284
285 RoundedImageView* image = new RoundedImageView(kProfileRoundedCornerRadius);
286 image->SetImage(ash::Shell::GetInstance()->tray_delegate()->GetUserImage(),
287 gfx::Size(kUserIconSize, kUserIconSize));
288 user_card_->AddChildView(image);
289
290 views::View* user = new views::View;
291 user->SetLayoutManager(new views::BoxLayout(
292 views::BoxLayout::kVertical, 0, 5, 0));
293 ash::SystemTrayDelegate* tray =
294 ash::Shell::GetInstance()->tray_delegate();
295 views::Label* username = new views::Label(tray->GetUserDisplayName());
296 username->SetHorizontalAlignment(gfx::ALIGN_LEFT);
297 user->AddChildView(username);
298
299 views::Label* email = new views::Label(UTF8ToUTF16(tray->GetUserEmail()));
300 email->SetFont(username->font().DeriveFont(-1));
301 email->SetHorizontalAlignment(gfx::ALIGN_LEFT);
302 email->SetEnabled(false);
303 user->AddChildView(email);
304
305 user_card_->AddChildView(user);
306 } 545 }
307 546
308 void AddLogoutButton(ash::user::LoginStatus login) { 547 void AddLogoutButton(ash::user::LoginStatus login) {
309 // A user should not be able to modify logged-in state when screen is 548 // A user should not be able to modify logged-in state when screen is
310 // locked. 549 // locked.
311 if (login == ash::user::LOGGED_IN_LOCKED) 550 if (login == ash::user::LOGGED_IN_LOCKED)
312 return; 551 return;
313 552
314 logout_button_ = new tray::PopupLogoutButton( 553 logout_button_ = new tray::PopupLogoutButton(
315 this, ash::user::GetLocalizedSignOutStringForStatus(login)); 554 this, ash::user::GetLocalizedSignOutStringForStatus(login));
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after
472 // Check for null to avoid crbug.com/150944. 711 // Check for null to avoid crbug.com/150944.
473 if (avatar_) { 712 if (avatar_) {
474 avatar_->SetImage( 713 avatar_->SetImage(
475 ash::Shell::GetInstance()->tray_delegate()->GetUserImage(), 714 ash::Shell::GetInstance()->tray_delegate()->GetUserImage(),
476 gfx::Size(kUserIconSize, kUserIconSize)); 715 gfx::Size(kUserIconSize, kUserIconSize));
477 } 716 }
478 } 717 }
479 718
480 } // namespace internal 719 } // namespace internal
481 } // namespace ash 720 } // namespace ash
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698