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

Side by Side Diff: chrome/browser/web_applications/web_app_mac.mm

Issue 15724019: Recreate shortcuts on app update. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Include profile dir in bundle id. Created 7 years, 6 months 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 #import "chrome/browser/web_applications/web_app_mac.h" 5 #import "chrome/browser/web_applications/web_app_mac.h"
6 6
7 #import <Carbon/Carbon.h> 7 #import <Carbon/Carbon.h>
8 #import <Cocoa/Cocoa.h> 8 #import <Cocoa/Cocoa.h>
9 9
10 #include "base/command_line.h" 10 #include "base/command_line.h"
(...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after
152 EndsWith(shim_path.RemoveExtension().value(), 152 EndsWith(shim_path.RemoveExtension().value(),
153 extension_id, 153 extension_id,
154 true /* case_sensitive */)) { 154 true /* case_sensitive */)) {
155 return true; 155 return true;
156 } 156 }
157 } 157 }
158 158
159 return false; 159 return false;
160 } 160 }
161 161
162 void DeleteShortcut(base::FilePath app_path) {
tapted 2013/06/18 06:54:59 const reference
tapted 2013/06/18 06:54:59 Maybe a more descriptive name, to distinguish it f
jackhou1 2013/06/18 08:32:40 Done.
jackhou1 2013/06/18 08:32:40 Done.
163 if (app_path.empty())
164 return;
165
166 file_util::Delete(app_path, true);
167 base::FilePath apps_folder = app_path.DirName();
168 if (file_util::IsDirectoryEmpty(apps_folder))
169 file_util::Delete(apps_folder, false);
170 }
171
162 } // namespace 172 } // namespace
163 173
164 namespace web_app { 174 namespace web_app {
165 175
166 const char kChromeAppDirName[] = "Chrome Apps.localized"; 176 const char kChromeAppDirName[] = "Chrome Apps.localized";
167 177
168 WebAppShortcutCreator::WebAppShortcutCreator( 178 WebAppShortcutCreator::WebAppShortcutCreator(
169 const base::FilePath& user_data_dir, 179 const base::FilePath& user_data_dir,
170 const ShellIntegration::ShortcutInfo& shortcut_info, 180 const ShellIntegration::ShortcutInfo& shortcut_info,
171 const string16& chrome_bundle_id) 181 const std::string& chrome_bundle_id)
172 : user_data_dir_(user_data_dir), 182 : user_data_dir_(user_data_dir),
173 info_(shortcut_info), 183 info_(shortcut_info),
174 chrome_bundle_id_(chrome_bundle_id) { 184 chrome_bundle_id_(chrome_bundle_id) {
175 } 185 }
176 186
177 WebAppShortcutCreator::~WebAppShortcutCreator() { 187 WebAppShortcutCreator::~WebAppShortcutCreator() {
178 } 188 }
179 189
180 base::FilePath WebAppShortcutCreator::GetShortcutPath() const { 190 base::FilePath WebAppShortcutCreator::GetShortcutName() const {
181 base::FilePath dst_path = GetDestinationPath();
182 if (dst_path.empty())
183 return dst_path;
184
185 base::FilePath app_name = internals::GetSanitizedFileName(UTF8ToUTF16( 191 base::FilePath app_name = internals::GetSanitizedFileName(UTF8ToUTF16(
186 info_.profile_path.BaseName().value() + " " + info_.extension_id)); 192 info_.profile_path.BaseName().value() + " " + info_.extension_id));
187 return dst_path.Append(app_name.ReplaceExtension("app")); 193 return app_name.ReplaceExtension("app");
188 } 194 }
189 195
190 bool WebAppShortcutCreator::CreateShortcut() { 196 bool WebAppShortcutCreator::BuildShortcut(
191 base::FilePath app_path = GetShortcutPath(); 197 const base::FilePath& staging_path) const {
192 base::FilePath app_name = app_path.BaseName();
193 base::FilePath dst_path = app_path.DirName();
194 if (app_path.empty() || !file_util::DirectoryExists(dst_path.DirName())) {
195 LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
196 return false;
197 }
198 if (!file_util::CreateDirectory(dst_path)) {
199 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
200 return false;
201 }
202
203 base::ScopedTempDir scoped_temp_dir;
204 if (!scoped_temp_dir.CreateUniqueTempDir())
205 return false;
206 base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
207
208 // Update the app's plist and icon in a temp directory. This works around 198 // Update the app's plist and icon in a temp directory. This works around
209 // a Finder bug where the app's icon doesn't properly update. 199 // a Finder bug where the app's icon doesn't properly update.
210 if (!file_util::CopyDirectory(GetAppLoaderPath(), staging_path, true)) { 200 if (!file_util::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
211 LOG(ERROR) << "Copying app to staging path: " << staging_path.value() 201 LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
212 << " failed"; 202 << " failed";
213 return false; 203 return false;
214 } 204 }
215 205
216 if (!UpdatePlist(staging_path)) 206 if (!UpdatePlist(staging_path))
217 return false; 207 return false;
218 208
219 if (!UpdateDisplayName(staging_path)) 209 if (!UpdateDisplayName(staging_path))
220 return false; 210 return false;
221 211
222 if (!UpdateIcon(staging_path)) 212 if (!UpdateIcon(staging_path))
223 return false; 213 return false;
224 214
225 if (!file_util::CopyDirectory(staging_path, dst_path, true)) { 215 return true;
226 LOG(ERROR) << "Copying app to dst path: " << dst_path.value() << " failed"; 216 }
217
218 bool WebAppShortcutCreator::CreateShortcutsIn(
219 const std::vector<base::FilePath>& folders) const {
220 base::ScopedTempDir scoped_temp_dir;
221 if (!scoped_temp_dir.CreateUniqueTempDir())
222 return false;
223
224 base::FilePath app_name = GetShortcutName();
225 base::FilePath staging_path =
226 scoped_temp_dir.path().Append(app_name);
227 if (!BuildShortcut(staging_path))
228 return false;
229
230 for (std::vector<base::FilePath>::const_iterator it = folders.begin();
231 it != folders.end(); ++it) {
232 const base::FilePath& dst_path = *it;
233 if (!file_util::CopyDirectory(staging_path, dst_path, true)) {
234 LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
235 << " failed";
236 return false;
237 }
238
239 base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
240 }
241
242 return true;
243 }
244
245 bool WebAppShortcutCreator::CreateShortcut() {
246 base::FilePath dst_path = GetDestinationPath();
247 if (dst_path.empty() || !file_util::DirectoryExists(dst_path.DirName())) {
248 LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
249 return false;
250 }
tapted 2013/06/18 06:54:59 nit: blank line after for early return
jackhou1 2013/06/18 08:32:40 Done.
251 if (!file_util::CreateDirectory(dst_path)) {
252 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
227 return false; 253 return false;
228 } 254 }
229 255
230 base::mac::RemoveQuarantineAttribute(app_path); 256 std::vector<base::FilePath> paths;
231 RevealGeneratedBundleInFinder(app_path); 257 paths.push_back(dst_path);
258 if (!CreateShortcutsIn(paths))
259 return false;
232 260
261 RevealAppShimInFinder();
233 return true; 262 return true;
234 } 263 }
235 264
265 void WebAppShortcutCreator::DeleteShortcuts() {
266 base::FilePath dst_path = GetDestinationPath();
267 if (!dst_path.empty())
268 DeleteShortcut(dst_path.Append(GetShortcutName()));
269 // In case the user has moved/renamed/copied the app bundle.
270 DeleteShortcut(GetAppBundleById(GetBundleIdentifier()));
271 }
272
273 bool WebAppShortcutCreator::UpdateShortcuts() {
274 base::FilePath dst_path = GetDestinationPath();
275 base::FilePath app_path = dst_path.Append(GetShortcutName());
276
277 // If the path does not exist, check if a matching bundle can be found
278 // elsewhere.
279 if (dst_path.empty() || !file_util::PathExists(app_path))
280 app_path = GetAppBundleById(GetBundleIdentifier());
281
282 if (app_path.empty())
283 return false;
284
285 file_util::Delete(app_path, true);
286
287 std::vector<base::FilePath> paths;
288 paths.push_back(app_path.DirName());
289 return CreateShortcutsIn(paths);
290 }
291
236 base::FilePath WebAppShortcutCreator::GetAppLoaderPath() const { 292 base::FilePath WebAppShortcutCreator::GetAppLoaderPath() const {
237 return base::mac::PathForFrameworkBundleResource( 293 return base::mac::PathForFrameworkBundleResource(
238 base::mac::NSToCFCast(@"app_mode_loader.app")); 294 base::mac::NSToCFCast(@"app_mode_loader.app"));
239 } 295 }
240 296
241 base::FilePath WebAppShortcutCreator::GetDestinationPath() const { 297 base::FilePath WebAppShortcutCreator::GetDestinationPath() const {
242 base::FilePath path = GetWritableApplicationsDirectory(); 298 base::FilePath path = GetWritableApplicationsDirectory();
243 if (path.empty()) 299 if (path.empty())
244 return path; 300 return path;
245 return path.Append(kChromeAppDirName); 301 return path.Append(kChromeAppDirName);
246 } 302 }
247 303
248 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const { 304 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
249 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id); 305 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
250 NSString* extension_title = base::SysUTF16ToNSString(info_.title); 306 NSString* extension_title = base::SysUTF16ToNSString(info_.title);
251 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec()); 307 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
252 NSString* chrome_bundle_id = base::SysUTF16ToNSString(chrome_bundle_id_); 308 NSString* chrome_bundle_id = base::SysUTF8ToNSString(chrome_bundle_id_);
253 NSDictionary* replacement_dict = 309 NSDictionary* replacement_dict =
254 [NSDictionary dictionaryWithObjectsAndKeys: 310 [NSDictionary dictionaryWithObjectsAndKeys:
255 extension_id, app_mode::kShortcutIdPlaceholder, 311 extension_id, app_mode::kShortcutIdPlaceholder,
256 extension_title, app_mode::kShortcutNamePlaceholder, 312 extension_title, app_mode::kShortcutNamePlaceholder,
257 extension_url, app_mode::kShortcutURLPlaceholder, 313 extension_url, app_mode::kShortcutURLPlaceholder,
258 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder, 314 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
259 nil]; 315 nil];
260 316
261 NSString* plist_path = base::mac::FilePathToNSString( 317 NSString* plist_path = base::mac::FilePathToNSString(
262 app_path.Append("Contents").Append("Info.plist")); 318 app_path.Append("Contents").Append("Info.plist"));
(...skipping 10 matching lines...) Expand all
273 // Remove leading and trailing '@'s. 329 // Remove leading and trailing '@'s.
274 NSString* variable = 330 NSString* variable =
275 [value substringWithRange:NSMakeRange(1, [value length] - 2)]; 331 [value substringWithRange:NSMakeRange(1, [value length] - 2)];
276 332
277 NSString* substitution = [replacement_dict valueForKey:variable]; 333 NSString* substitution = [replacement_dict valueForKey:variable];
278 if (substitution) 334 if (substitution)
279 [plist setObject:substitution forKey:key]; 335 [plist setObject:substitution forKey:key];
280 } 336 }
281 337
282 // 2. Fill in other values. 338 // 2. Fill in other values.
283 [plist setObject:GetBundleIdentifier(plist) 339 [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
284 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; 340 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
285 [plist setObject:base::mac::FilePathToNSString(user_data_dir_) 341 [plist setObject:base::mac::FilePathToNSString(user_data_dir_)
286 forKey:app_mode::kCrAppModeUserDataDirKey]; 342 forKey:app_mode::kCrAppModeUserDataDirKey];
287 [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName()) 343 [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName())
288 forKey:app_mode::kCrAppModeProfileDirKey]; 344 forKey:app_mode::kCrAppModeProfileDirKey];
289 [plist setObject:base::SysUTF8ToNSString(info_.profile_name) 345 [plist setObject:base::SysUTF8ToNSString(info_.profile_name)
290 forKey:app_mode::kCrAppModeProfileNameKey]; 346 forKey:app_mode::kCrAppModeProfileNameKey];
291 [plist setObject:[NSNumber numberWithBool:YES] 347 [plist setObject:[NSNumber numberWithBool:YES]
292 forKey:app_mode::kLSHasLocalizedDisplayNameKey]; 348 forKey:app_mode::kLSHasLocalizedDisplayNameKey];
293 349
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
349 if (!image_added) 405 if (!image_added)
350 return false; 406 return false;
351 407
352 base::FilePath resources_path = GetResourcesPath(app_path); 408 base::FilePath resources_path = GetResourcesPath(app_path);
353 if (!file_util::CreateDirectory(resources_path)) 409 if (!file_util::CreateDirectory(resources_path))
354 return false; 410 return false;
355 411
356 return icon_family.WriteDataToFile(resources_path.Append("app.icns")); 412 return icon_family.WriteDataToFile(resources_path.Append("app.icns"));
357 } 413 }
358 414
359 NSString* WebAppShortcutCreator::GetBundleIdentifier(NSDictionary* plist) const 415 base::FilePath WebAppShortcutCreator::GetAppBundleById(
360 { 416 const std::string& bundle_id) const {
361 NSString* bundle_id_template = 417 base::mac::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
362 base::mac::ObjCCast<NSString>( 418 base::SysUTF8ToCFStringRef(bundle_id));
363 [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]); 419 CFURLRef url_ref = NULL;
364 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id); 420 OSStatus status = LSFindApplicationForInfo(
365 NSString* placeholder = 421 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
366 [NSString stringWithFormat:@"@%@@", app_mode::kShortcutIdPlaceholder]; 422 base::mac::ScopedCFTypeRef<CFURLRef> url(url_ref);
tapted 2013/06/18 06:54:59 move this below checking status != noErr?
jackhou1 2013/06/18 08:32:40 Done.
367 NSString* bundle_id = 423
368 [bundle_id_template 424 if (status != noErr)
369 stringByReplacingOccurrencesOfString:placeholder 425 return base::FilePath();
370 withString:extension_id]; 426
427 NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
428 return base::FilePath([path_string fileSystemRepresentation]);
429 }
430
431 std::string WebAppShortcutCreator::GetBundleIdentifier() const {
432 // Replace spaces in the profile path with hyphen.
433 std::string normalized_profile_path;
434 ReplaceChars(info_.profile_path.BaseName().value(),
435 " ", "-", &normalized_profile_path);
436
437 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
438 std::string bundle_id =
439 chrome_bundle_id_ + std::string(".app.") +
440 normalized_profile_path + "-" + info_.extension_id;
441
371 return bundle_id; 442 return bundle_id;
372 } 443 }
373 444
374 void WebAppShortcutCreator::RevealGeneratedBundleInFinder( 445 void WebAppShortcutCreator::RevealAppShimInFinder() const {
375 const base::FilePath& generated_bundle) const { 446 base::FilePath dst_path = GetDestinationPath();
447 if (dst_path.empty())
448 return;
449
450 base::FilePath app_path = dst_path.Append(GetShortcutName());
376 [[NSWorkspace sharedWorkspace] 451 [[NSWorkspace sharedWorkspace]
377 selectFile:base::mac::FilePathToNSString(generated_bundle) 452 selectFile:base::mac::FilePathToNSString(app_path)
378 inFileViewerRootedAtPath:nil]; 453 inFileViewerRootedAtPath:nil];
379 } 454 }
380 455
381 void LaunchShimOnFileThread( 456 void LaunchShimOnFileThread(
382 const ShellIntegration::ShortcutInfo& shortcut_info) { 457 const ShellIntegration::ShortcutInfo& shortcut_info) {
383 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 458 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
384 base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info); 459 base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info);
385 if (shim_path.empty()) 460 if (shim_path.empty())
386 return; 461 return;
387 462
388 CommandLine command_line(CommandLine::NO_PROGRAM); 463 CommandLine command_line(CommandLine::NO_PROGRAM);
389 command_line.AppendSwitch(app_mode::kNoLaunchApp); 464 command_line.AppendSwitch(app_mode::kNoLaunchApp);
390 base::mac::OpenApplicationWithPath(shim_path, command_line, NULL); 465 base::mac::OpenApplicationWithPath(shim_path, command_line, NULL);
391 } 466 }
392 467
393 } // namespace 468 } // namespace
394 469
395 namespace web_app { 470 namespace web_app {
396 471
397 base::FilePath GetAppInstallPath( 472 base::FilePath GetAppInstallPath(
398 const ShellIntegration::ShortcutInfo& shortcut_info) { 473 const ShellIntegration::ShortcutInfo& shortcut_info) {
399 WebAppShortcutCreator shortcut_creator(base::FilePath(), 474 WebAppShortcutCreator shortcut_creator(base::FilePath(),
400 shortcut_info, 475 shortcut_info,
401 string16()); 476 std::string());
402 return shortcut_creator.GetShortcutPath(); 477 base::FilePath dst_path = shortcut_creator.GetDestinationPath();
478 return dst_path.empty() ?
479 base::FilePath() : dst_path.Append(shortcut_creator.GetShortcutName());
403 } 480 }
404 481
405 void MaybeLaunchShortcut(const ShellIntegration::ShortcutInfo& shortcut_info) { 482 void MaybeLaunchShortcut(const ShellIntegration::ShortcutInfo& shortcut_info) {
406 if (!CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppShims)) 483 if (!CommandLine::ForCurrentProcess()->HasSwitch(switches::kEnableAppShims))
407 return; 484 return;
408 485
409 content::BrowserThread::PostTask( 486 content::BrowserThread::PostTask(
410 content::BrowserThread::FILE, FROM_HERE, 487 content::BrowserThread::FILE, FROM_HERE,
411 base::Bind(&LaunchShimOnFileThread, shortcut_info)); 488 base::Bind(&LaunchShimOnFileThread, shortcut_info));
412 } 489 }
413 490
414 namespace internals { 491 namespace internals {
415 492
416 base::FilePath GetAppBundleByExtensionId(std::string extension_id) {
417 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
418 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp.
419 std::string bundle_id =
420 base::mac::BaseBundleID() + std::string(".app.") + extension_id;
421 base::mac::ScopedCFTypeRef<CFStringRef> bundle_id_cf(
422 base::SysUTF8ToCFStringRef(bundle_id));
423 CFURLRef url_ref = NULL;
424 OSStatus status = LSFindApplicationForInfo(
425 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref);
426 base::mac::ScopedCFTypeRef<CFURLRef> url(url_ref);
427
428 if (status != noErr)
429 return base::FilePath();
430
431 NSString* path_string = [base::mac::CFToNSCast(url.get()) path];
432 return base::FilePath([path_string fileSystemRepresentation]);
433 }
434
435 bool CreatePlatformShortcuts( 493 bool CreatePlatformShortcuts(
436 const base::FilePath& web_app_path, 494 const base::FilePath& web_app_path,
437 const ShellIntegration::ShortcutInfo& shortcut_info, 495 const ShellIntegration::ShortcutInfo& shortcut_info,
438 const ShellIntegration::ShortcutLocations& /*creation_locations*/) { 496 const ShellIntegration::ShortcutLocations& creation_locations) {
439 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 497 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
440 string16 bundle_id = UTF8ToUTF16(base::mac::BaseBundleID()); 498 WebAppShortcutCreator shortcut_creator(
441 WebAppShortcutCreator shortcut_creator(web_app_path, shortcut_info, 499 web_app_path, shortcut_info, base::mac::BaseBundleID());
442 bundle_id);
443 return shortcut_creator.CreateShortcut(); 500 return shortcut_creator.CreateShortcut();
444 } 501 }
445 502
446 void DeletePlatformShortcuts( 503 void DeletePlatformShortcuts(
447 const base::FilePath& web_app_path, 504 const base::FilePath& web_app_path,
448 const ShellIntegration::ShortcutInfo& info) { 505 const ShellIntegration::ShortcutInfo& shortcut_info) {
449 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 506 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
450 507 WebAppShortcutCreator shortcut_creator(
451 base::FilePath bundle_path = GetAppBundleByExtensionId(info.extension_id); 508 web_app_path, shortcut_info, base::mac::BaseBundleID());
452 file_util::Delete(bundle_path, true); 509 shortcut_creator.DeleteShortcuts();
453 } 510 }
454 511
455 void UpdatePlatformShortcuts( 512 void UpdatePlatformShortcuts(
456 const base::FilePath& web_app_path, 513 const base::FilePath& web_app_path,
457 const string16& old_app_title, 514 const string16& old_app_title,
458 const ShellIntegration::ShortcutInfo& shortcut_info) { 515 const ShellIntegration::ShortcutInfo& shortcut_info) {
459 // TODO(benwells): Implement this when shortcuts / weblings are enabled on 516 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
460 // mac. 517 WebAppShortcutCreator shortcut_creator(
518 web_app_path, shortcut_info, base::mac::BaseBundleID());
519 shortcut_creator.UpdateShortcuts();
461 } 520 }
462 521
463 } // namespace internals 522 } // namespace internals
464 523
465 } // namespace web_app 524 } // namespace web_app
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698