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 3 // found in the LICENSE file. 4 5 #import "chrome/browser/web_applications/web_app_mac.h" 6 7 #import <Carbon/Carbon.h> 8 #import <Cocoa/Cocoa.h> 9 10 #include "apps/app_shim/app_shim_mac.h" 11 #include "base/command_line.h" 12 #include "base/file_util.h" 13 #include "base/files/file_enumerator.h" 14 #include "base/files/scoped_temp_dir.h" 15 #include "base/mac/bundle_locations.h" 16 #include "base/mac/foundation_util.h" 17 #include "base/mac/launch_services_util.h" 18 #include "base/mac/mac_logging.h" 19 #include "base/mac/mac_util.h" 20 #include "base/mac/scoped_cftyperef.h" 21 #include "base/path_service.h" 22 #include "base/strings/string_util.h" 23 #include "base/strings/sys_string_conversions.h" 24 #include "base/strings/utf_string_conversions.h" 25 #include "chrome/browser/browser_process.h" 26 #include "chrome/browser/mac/dock.h" 27 #include "chrome/browser/ui/web_applications/web_app_ui.h" 28 #include "chrome/browser/web_applications/web_app.h" 29 #include "chrome/common/chrome_constants.h" 30 #include "chrome/common/chrome_paths.h" 31 #include "chrome/common/chrome_switches.h" 32 #include "chrome/common/chrome_version_info.h" 33 #include "chrome/common/extensions/extension.h" 34 #include "chrome/common/mac/app_mode_common.h" 35 #include "content/public/browser/browser_thread.h" 36 #include "grit/chromium_strings.h" 37 #include "skia/ext/skia_utils_mac.h" 38 #include "third_party/skia/include/core/SkBitmap.h" 39 #include "third_party/skia/include/core/SkColor.h" 40 #include "ui/base/l10n/l10n_util.h" 41 #include "ui/base/l10n/l10n_util_mac.h" 42 #include "ui/gfx/image/image_family.h" 43 44 namespace { 45 46 // Launch Services Key to run as an agent app, which doesn't launch in the dock. 47 NSString* const kLSUIElement = @"LSUIElement"; 48 49 class ScopedCarbonHandle { 50 public: 51 ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) { 52 DCHECK(handle_); 53 DCHECK_EQ(noErr, MemError()); 54 } 55 ~ScopedCarbonHandle() { DisposeHandle(handle_); } 56 57 Handle Get() { return handle_; } 58 char* Data() { return *handle_; } 59 size_t HandleSize() const { return GetHandleSize(handle_); } 60 61 IconFamilyHandle GetAsIconFamilyHandle() { 62 return reinterpret_cast<IconFamilyHandle>(handle_); 63 } 64 65 bool WriteDataToFile(const base::FilePath& path) { 66 NSData* data = [NSData dataWithBytes:Data() 67 length:HandleSize()]; 68 return [data writeToFile:base::mac::FilePathToNSString(path) 69 atomically:NO]; 70 } 71 72 private: 73 Handle handle_; 74 }; 75 76 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) { 77 CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize()); 78 79 char* argb = handle->Data(); 80 SkAutoLockPixels lock(bitmap); 81 for (int y = 0; y < bitmap.height(); ++y) { 82 for (int x = 0; x < bitmap.width(); ++x) { 83 SkColor pixel = bitmap.getColor(x, y); 84 argb[0] = SkColorGetA(pixel); 85 argb[1] = SkColorGetR(pixel); 86 argb[2] = SkColorGetG(pixel); 87 argb[3] = SkColorGetB(pixel); 88 argb += 4; 89 } 90 } 91 } 92 93 // Adds |image| to |icon_family|. Returns true on success, false on failure. 94 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family, 95 const gfx::Image& image) { 96 // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will 97 // have all the representations desired here for mac, from the kDesiredSizes 98 // array in web_app_ui.cc. 99 SkBitmap bitmap = image.AsBitmap(); 100 if (bitmap.config() != SkBitmap::kARGB_8888_Config || 101 bitmap.width() != bitmap.height()) { 102 return false; 103 } 104 105 OSType icon_type; 106 switch (bitmap.width()) { 107 case 512: 108 icon_type = kIconServices512PixelDataARGB; 109 break; 110 case 256: 111 icon_type = kIconServices256PixelDataARGB; 112 break; 113 case 128: 114 icon_type = kIconServices128PixelDataARGB; 115 break; 116 case 48: 117 icon_type = kIconServices48PixelDataARGB; 118 break; 119 case 32: 120 icon_type = kIconServices32PixelDataARGB; 121 break; 122 case 16: 123 icon_type = kIconServices16PixelDataARGB; 124 break; 125 default: 126 return false; 127 } 128 129 ScopedCarbonHandle raw_data(bitmap.getSize()); 130 ConvertSkiaToARGB(bitmap, &raw_data); 131 OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get()); 132 DCHECK_EQ(noErr, result); 133 return result == noErr; 134 } 135 136 base::FilePath GetWritableApplicationsDirectory() { 137 base::FilePath path; 138 if (base::mac::GetLocalDirectory(NSApplicationDirectory, &path) && 139 base::PathIsWritable(path)) { 140 return path; 141 } 142 if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) 143 return path; 144 return base::FilePath(); 145 } 146 147 // Given the path to an app bundle, return the resources directory. 148 base::FilePath GetResourcesPath(const base::FilePath& app_path) { 149 return app_path.Append("Contents").Append("Resources"); 150 } 151 152 bool HasExistingExtensionShim(const base::FilePath& destination_directory, 153 const std::string& extension_id, 154 const base::FilePath& own_basename) { 155 // Check if there any any other shims for the same extension. 156 base::FileEnumerator enumerator(destination_directory, 157 false /* recursive */, 158 base::FileEnumerator::DIRECTORIES); 159 for (base::FilePath shim_path = enumerator.Next(); 160 !shim_path.empty(); shim_path = enumerator.Next()) { 161 if (shim_path.BaseName() != own_basename && 162 EndsWith(shim_path.RemoveExtension().value(), 163 extension_id, 164 true /* case_sensitive */)) { 165 return true; 166 } 167 } 168 169 return false; 170 } 171 172 // Given the path to an app bundle, return the path to the Info.plist file. 173 NSString* GetPlistPath(const base::FilePath& bundle_path) { 174 return base::mac::FilePathToNSString( 175 bundle_path.Append("Contents").Append("Info.plist")); 176 } 177 178 NSMutableDictionary* ReadPlist(NSString* plist_path) { 179 return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path]; 180 } 181 182 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in 183 // the Info.plist starts with the current user_data_dir. This uses starts with 184 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir 185 // or the app_data_path. 186 bool HasSameUserDataDir(const base::FilePath& bundle_path) { 187 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); 188 base::FilePath user_data_dir; 189 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); 190 DCHECK(!user_data_dir.empty()); 191 return StartsWithASCII( 192 base::SysNSStringToUTF8( 193 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]), 194 user_data_dir.value(), 195 true /* case_sensitive */); 196 } 197 198 void LaunchShimOnFileThread( 199 const ShellIntegration::ShortcutInfo& shortcut_info) { 200 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 201 base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info); 202 203 if (shim_path.empty() || 204 !base::PathExists(shim_path) || 205 !HasSameUserDataDir(shim_path)) { 206 // The user may have deleted the copy in the Applications folder, use the 207 // one in the web app's app_data_path. 208 base::FilePath app_data_path = web_app::GetWebAppDataDirectory( 209 shortcut_info.profile_path, shortcut_info.extension_id, GURL()); 210 shim_path = app_data_path.Append(shim_path.BaseName()); 211 } 212 213 if (!base::PathExists(shim_path)) 214 return; 215 216 CommandLine command_line(CommandLine::NO_PROGRAM); 217 command_line.AppendSwitch(app_mode::kNoLaunchApp); 218 // Launch without activating (kLSLaunchDontSwitch). 219 base::mac::OpenApplicationWithPath( 220 shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL); 221 } 222 223 base::FilePath GetLocalizableAppShortcutsSubdirName() { 224 #if defined(GOOGLE_CHROME_BUILD) 225 static const char kChromeAppDirName[] = "Chrome Apps.localized"; 226 static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized"; 227 228 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel(); 229 if (channel == chrome::VersionInfo::CHANNEL_CANARY) 230 return base::FilePath(kChromeCanaryAppDirName); 231 232 return base::FilePath(kChromeAppDirName); 233 #else 234 static const char kChromiumAppDirName[] = "Chromium Apps.localized"; 235 236 return base::FilePath(kChromiumAppDirName); 237 #endif 238 } 239 240 // Adds a localized strings file for the Chrome Apps directory using the current 241 // locale. OSX will use this for the display name. 242 // + Chrome Apps.localized (|apps_directory|) 243 // | + .localized 244 // | | en.strings 245 // | | de.strings 246 void UpdateAppShortcutsSubdirLocalizedName( 247 const base::FilePath& apps_directory) { 248 base::FilePath localized = apps_directory.Append(".localized"); 249 if (!file_util::CreateDirectory(localized)) 250 return; 251 252 base::FilePath directory_name = apps_directory.BaseName().RemoveExtension(); 253 string16 localized_name = web_app::GetAppShortcutsSubdirName(); 254 NSDictionary* strings_dict = @{ 255 base::mac::FilePathToNSString(directory_name) : 256 base::SysUTF16ToNSString(localized_name) 257 }; 258 259 std::string locale = l10n_util::NormalizeLocale( 260 l10n_util::GetApplicationLocale(std::string())); 261 262 NSString* strings_path = base::mac::FilePathToNSString( 263 localized.Append(locale + ".strings")); 264 [strings_dict writeToFile:strings_path 265 atomically:YES]; 266 } 267 268 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) { 269 DCHECK(!app_path.empty()); 270 base::DeleteFile(app_path, true); 271 base::FilePath apps_folder = app_path.DirName(); 272 if (file_util::IsDirectoryEmpty(apps_folder)) 273 base::DeleteFile(apps_folder, false); 274 } 275 276 bool IsShimForProfile(const base::FilePath& base_name, 277 const std::string& profile_base_name) { 278 if (!StartsWithASCII(base_name.value(), profile_base_name, true)) 279 return false; 280 281 if (base_name.Extension() != ".app") 282 return false; 283 284 std::string app_id = base_name.RemoveExtension().value(); 285 // Strip (profile_base_name + " ") from the start. 286 app_id = app_id.substr(profile_base_name.size() + 1); 287 return extensions::Extension::IdIsValid(app_id); 288 } 289 290 std::vector<base::FilePath> GetAllAppBundlesInPath( 291 const base::FilePath& internal_shortcut_path, 292 const std::string& profile_base_name) { 293 std::vector<base::FilePath> bundle_paths; 294 295 base::FileEnumerator enumerator(internal_shortcut_path, 296 true /* recursive */, 297 base::FileEnumerator::DIRECTORIES); 298 for (base::FilePath bundle_path = enumerator.Next(); 299 !bundle_path.empty(); bundle_path = enumerator.Next()) { 300 if (IsShimForProfile(bundle_path.BaseName(), profile_base_name)) 301 bundle_paths.push_back(bundle_path); 302 } 303 304 return bundle_paths; 305 } 306 307 ShellIntegration::ShortcutInfo BuildShortcutInfoFromBundle( 308 const base::FilePath& bundle_path) { 309 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); 310 311 ShellIntegration::ShortcutInfo shortcut_info; 312 shortcut_info.extension_id = base::SysNSStringToUTF8( 313 [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]); 314 shortcut_info.is_platform_app = true; 315 shortcut_info.url = GURL(base::SysNSStringToUTF8( 316 [plist valueForKey:app_mode::kCrAppModeShortcutURLKey])); 317 shortcut_info.title = base::SysNSStringToUTF16( 318 [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]); 319 shortcut_info.profile_name = base::SysNSStringToUTF8( 320 [plist valueForKey:app_mode::kCrAppModeProfileNameKey]); 321 322 // Figure out the profile_path. Since the user_data_dir could contain the 323 // path to the web app data dir. 324 base::FilePath user_data_dir = base::mac::NSStringToFilePath( 325 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]); 326 base::FilePath profile_base_name = base::mac::NSStringToFilePath( 327 [plist valueForKey:app_mode::kCrAppModeProfileDirKey]); 328 if (user_data_dir.DirName().DirName().BaseName() == profile_base_name) 329 shortcut_info.profile_path = user_data_dir.DirName().DirName(); 330 else 331 shortcut_info.profile_path = user_data_dir.Append(profile_base_name); 332 333 return shortcut_info; 334 } 335 336 } // namespace 337 338 namespace web_app { 339 340 341 WebAppShortcutCreator::WebAppShortcutCreator( 342 const base::FilePath& app_data_path, 343 const ShellIntegration::ShortcutInfo& shortcut_info, 344 const std::string& chrome_bundle_id) 345 : app_data_path_(app_data_path), 346 info_(shortcut_info), 347 chrome_bundle_id_(chrome_bundle_id) { 348 } 349 350 WebAppShortcutCreator::~WebAppShortcutCreator() { 351 } 352 353 base::FilePath WebAppShortcutCreator::GetShortcutName() const { 354 std::string app_name; 355 // Check if there should be a separate shortcut made for different profiles. 356 // Such shortcuts will have a |profile_name| set on the ShortcutInfo, 357 // otherwise it will be empty. 358 if (!info_.profile_name.empty()) { 359 app_name += info_.profile_path.BaseName().value(); 360 app_name += ' '; 361 } 362 app_name += info_.extension_id; 363 return base::FilePath(app_name).ReplaceExtension("app"); 364 } 365 366 bool WebAppShortcutCreator::BuildShortcut( 367 const base::FilePath& staging_path) const { 368 // Update the app's plist and icon in a temp directory. This works around 369 // a Finder bug where the app's icon doesn't properly update. 370 if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) { 371 LOG(ERROR) << "Copying app to staging path: " << staging_path.value() 372 << " failed."; 373 return false; 374 } 375 376 if (!UpdatePlist(staging_path)) 377 return false; 378 379 if (!UpdateDisplayName(staging_path)) 380 return false; 381 382 if (!UpdateIcon(staging_path)) 383 return false; 384 385 return true; 386 } 387 388 size_t WebAppShortcutCreator::CreateShortcutsIn( 389 const std::vector<base::FilePath>& folders) const { 390 size_t succeeded = 0; 391 392 base::ScopedTempDir scoped_temp_dir; 393 if (!scoped_temp_dir.CreateUniqueTempDir()) 394 return 0; 395 396 base::FilePath app_name = GetShortcutName(); 397 base::FilePath staging_path = scoped_temp_dir.path().Append(app_name); 398 if (!BuildShortcut(staging_path)) 399 return 0; 400 401 for (std::vector<base::FilePath>::const_iterator it = folders.begin(); 402 it != folders.end(); ++it) { 403 const base::FilePath& dst_path = *it; 404 if (!file_util::CreateDirectory(dst_path)) { 405 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed."; 406 return succeeded; 407 } 408 409 if (!base::CopyDirectory(staging_path, dst_path, true)) { 410 LOG(ERROR) << "Copying app to dst path: " << dst_path.value() 411 << " failed"; 412 return succeeded; 413 } 414 415 base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name)); 416 ++succeeded; 417 } 418 419 return succeeded; 420 } 421 422 bool WebAppShortcutCreator::CreateShortcuts( 423 ShortcutCreationReason creation_reason) { 424 const base::FilePath applications_path = GetDestinationPath(); 425 if (applications_path.empty() || 426 !base::DirectoryExists(applications_path.DirName())) { 427 LOG(ERROR) << "Couldn't find an Applications directory to copy app to."; 428 return false; 429 } 430 431 UpdateAppShortcutsSubdirLocalizedName(applications_path); 432 433 // If non-nil, this path is added to the OSX Dock after creating shortcuts. 434 NSString* path_to_add_to_dock = nil; 435 436 std::vector<base::FilePath> paths; 437 438 // For the app list shim, place a copy in Chrome's user data dir for use in 439 // the OSX Dock, and do not create the copy in the profile dir. This is done 440 // because the kAppLauncherHasBeenEnabled preference is tied to the local 441 // state, rather than per-profile. 442 const bool is_app_list = info_.extension_id == app_mode::kAppListModeId; 443 if (is_app_list) { 444 base::FilePath user_data_dir; 445 CHECK(PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)); 446 path_to_add_to_dock = base::SysUTF8ToNSString( 447 user_data_dir.Append(GetShortcutName()).AsUTF8Unsafe()); 448 paths.push_back(user_data_dir); 449 } else { 450 paths.push_back(app_data_path_); 451 } 452 paths.push_back(applications_path); 453 454 size_t success_count = CreateShortcutsIn(paths); 455 if (success_count == 0) 456 return false; 457 458 if (!is_app_list) 459 UpdateInternalBundleIdentifier(); 460 461 if (success_count != paths.size()) 462 return false; 463 464 if (path_to_add_to_dock) 465 dock::AddIcon(path_to_add_to_dock, nil); 466 467 if (creation_reason == SHORTCUT_CREATION_BY_USER) 468 RevealAppShimInFinder(); 469 470 return true; 471 } 472 473 void WebAppShortcutCreator::DeleteShortcuts() { 474 base::FilePath dst_path = GetDestinationPath(); 475 if (!dst_path.empty()) { 476 base::FilePath bundle_path = dst_path.Append(GetShortcutName()); 477 if (HasSameUserDataDir(bundle_path)) 478 DeletePathAndParentIfEmpty(bundle_path); 479 } 480 481 // In case the user has moved/renamed/copied the app bundle. 482 base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier()); 483 if (!bundle_path.empty() && HasSameUserDataDir(bundle_path)) 484 base::DeleteFile(bundle_path, true); 485 486 // Delete the internal one. 487 DeletePathAndParentIfEmpty(app_data_path_.Append(GetShortcutName())); 488 } 489 490 bool WebAppShortcutCreator::UpdateShortcuts() { 491 std::vector<base::FilePath> paths; 492 base::DeleteFile(app_data_path_.Append(GetShortcutName()), true); 493 paths.push_back(app_data_path_); 494 495 base::FilePath dst_path = GetDestinationPath(); 496 base::FilePath app_path = dst_path.Append(GetShortcutName()); 497 498 // If the path does not exist, check if a matching bundle can be found 499 // elsewhere. 500 if (dst_path.empty() || !base::PathExists(app_path)) 501 app_path = GetAppBundleById(GetBundleIdentifier()); 502 503 if (!app_path.empty()) { 504 base::DeleteFile(app_path, true); 505 paths.push_back(app_path.DirName()); 506 } 507 508 size_t success_count = CreateShortcutsIn(paths); 509 if (success_count == 0) 510 return false; 511 512 UpdateInternalBundleIdentifier(); 513 return success_count == paths.size() && !app_path.empty(); 514 } 515 516 base::FilePath WebAppShortcutCreator::GetAppLoaderPath() const { 517 return base::mac::PathForFrameworkBundleResource( 518 base::mac::NSToCFCast(@"app_mode_loader.app")); 519 } 520 521 base::FilePath WebAppShortcutCreator::GetDestinationPath() const { 522 base::FilePath path = GetWritableApplicationsDirectory(); 523 if (path.empty()) 524 return path; 525 return path.Append(GetLocalizableAppShortcutsSubdirName()); 526 } 527 528 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const { 529 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id); 530 NSString* extension_title = base::SysUTF16ToNSString(info_.title); 531 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec()); 532 NSString* chrome_bundle_id = base::SysUTF8ToNSString(chrome_bundle_id_); 533 NSDictionary* replacement_dict = 534 [NSDictionary dictionaryWithObjectsAndKeys: 535 extension_id, app_mode::kShortcutIdPlaceholder, 536 extension_title, app_mode::kShortcutNamePlaceholder, 537 extension_url, app_mode::kShortcutURLPlaceholder, 538 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder, 539 nil]; 540 541 NSString* plist_path = GetPlistPath(app_path); 542 NSMutableDictionary* plist = ReadPlist(plist_path); 543 NSArray* keys = [plist allKeys]; 544 545 // 1. Fill in variables. 546 for (id key in keys) { 547 NSString* value = [plist valueForKey:key]; 548 if (![value isKindOfClass:[NSString class]] || [value length] < 2) 549 continue; 550 551 // Remove leading and trailing '@'s. 552 NSString* variable = 553 [value substringWithRange:NSMakeRange(1, [value length] - 2)]; 554 555 NSString* substitution = [replacement_dict valueForKey:variable]; 556 if (substitution) 557 [plist setObject:substitution forKey:key]; 558 } 559 560 // 2. Fill in other values. 561 [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier()) 562 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; 563 [plist setObject:base::mac::FilePathToNSString(app_data_path_) 564 forKey:app_mode::kCrAppModeUserDataDirKey]; 565 [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName()) 566 forKey:app_mode::kCrAppModeProfileDirKey]; 567 [plist setObject:base::SysUTF8ToNSString(info_.profile_name) 568 forKey:app_mode::kCrAppModeProfileNameKey]; 569 [plist setObject:[NSNumber numberWithBool:YES] 570 forKey:app_mode::kLSHasLocalizedDisplayNameKey]; 571 if (info_.extension_id == app_mode::kAppListModeId) { 572 // Prevent the app list from bouncing in the dock, and getting a run light. 573 [plist setObject:[NSNumber numberWithBool:YES] 574 forKey:kLSUIElement]; 575 } 576 577 base::FilePath app_name = app_path.BaseName().RemoveExtension(); 578 [plist setObject:base::mac::FilePathToNSString(app_name) 579 forKey:base::mac::CFToNSCast(kCFBundleNameKey)]; 580 581 return [plist writeToFile:plist_path 582 atomically:YES]; 583 } 584 585 bool WebAppShortcutCreator::UpdateDisplayName( 586 const base::FilePath& app_path) const { 587 // OSX searches for the best language in the order of preferred languages. 588 // Since we only have one localization directory, it will choose this one. 589 base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj"); 590 if (!file_util::CreateDirectory(localized_dir)) 591 return false; 592 593 NSString* bundle_name = base::SysUTF16ToNSString(info_.title); 594 NSString* display_name = base::SysUTF16ToNSString(info_.title); 595 if (HasExistingExtensionShim(GetDestinationPath(), 596 info_.extension_id, 597 app_path.BaseName())) { 598 display_name = [bundle_name 599 stringByAppendingString:base::SysUTF8ToNSString( 600 " (" + info_.profile_name + ")")]; 601 } 602 603 NSDictionary* strings_plist = @{ 604 base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name, 605 app_mode::kCFBundleDisplayNameKey : display_name 606 }; 607 608 NSString* localized_path = base::mac::FilePathToNSString( 609 localized_dir.Append("InfoPlist.strings")); 610 return [strings_plist writeToFile:localized_path 611 atomically:YES]; 612 } 613 614 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const { 615 if (info_.favicon.empty()) 616 return true; 617 618 ScopedCarbonHandle icon_family(0); 619 bool image_added = false; 620 for (gfx::ImageFamily::const_iterator it = info_.favicon.begin(); 621 it != info_.favicon.end(); ++it) { 622 if (it->IsEmpty()) 623 continue; 624 625 // Missing an icon size is not fatal so don't fail if adding the bitmap 626 // doesn't work. 627 if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it)) 628 continue; 629 630 image_added = true; 631 } 632 633 if (!image_added) 634 return false; 635 636 base::FilePath resources_path = GetResourcesPath(app_path); 637 if (!file_util::CreateDirectory(resources_path)) 638 return false; 639 640 return icon_family.WriteDataToFile(resources_path.Append("app.icns")); 641 } 642 643 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const { 644 NSString* plist_path = GetPlistPath(app_data_path_.Append(GetShortcutName())); 645 NSMutableDictionary* plist = ReadPlist(plist_path); 646 647 [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier()) 648 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; 649 return [plist writeToFile:plist_path 650 atomically:YES]; 651 } 652 653 base::FilePath WebAppShortcutCreator::GetAppBundleById( 654 const std::string& bundle_id) const { 655 base::ScopedCFTypeRef<CFStringRef> bundle_id_cf( 656 base::SysUTF8ToCFStringRef(bundle_id)); 657 CFURLRef url_ref = NULL; 658 OSStatus status = LSFindApplicationForInfo( 659 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref); 660 if (status != noErr) 661 return base::FilePath(); 662 663 base::ScopedCFTypeRef<CFURLRef> url(url_ref); 664 NSString* path_string = [base::mac::CFToNSCast(url.get()) path]; 665 return base::FilePath([path_string fileSystemRepresentation]); 666 } 667 668 std::string WebAppShortcutCreator::GetBundleIdentifier() const { 669 // Replace spaces in the profile path with hyphen. 670 std::string normalized_profile_path; 671 ReplaceChars(info_.profile_path.BaseName().value(), 672 " ", "-", &normalized_profile_path); 673 674 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp. 675 std::string bundle_id = 676 chrome_bundle_id_ + std::string(".app.") + 677 normalized_profile_path + "-" + info_.extension_id; 678 679 return bundle_id; 680 } 681 682 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const { 683 return GetBundleIdentifier() + "-internal"; 684 } 685 686 void WebAppShortcutCreator::RevealAppShimInFinder() const { 687 base::FilePath dst_path = GetDestinationPath(); 688 if (dst_path.empty()) 689 return; 690 691 base::FilePath app_path = dst_path.Append(GetShortcutName()); 692 [[NSWorkspace sharedWorkspace] 693 selectFile:base::mac::FilePathToNSString(app_path) 694 inFileViewerRootedAtPath:nil]; 695 } 696 697 base::FilePath GetAppInstallPath( 698 const ShellIntegration::ShortcutInfo& shortcut_info) { 699 WebAppShortcutCreator shortcut_creator(base::FilePath(), 700 shortcut_info, 701 std::string()); 702 base::FilePath dst_path = shortcut_creator.GetDestinationPath(); 703 return dst_path.empty() ? 704 base::FilePath() : dst_path.Append(shortcut_creator.GetShortcutName()); 705 } 706 707 void MaybeLaunchShortcut(const ShellIntegration::ShortcutInfo& shortcut_info) { 708 if (!apps::IsAppShimsEnabled()) 709 return; 710 711 content::BrowserThread::PostTask( 712 content::BrowserThread::FILE, FROM_HERE, 713 base::Bind(&LaunchShimOnFileThread, shortcut_info)); 714 } 715 716 namespace internals { 717 718 bool CreatePlatformShortcuts( 719 const base::FilePath& app_data_path, 720 const ShellIntegration::ShortcutInfo& shortcut_info, 721 const ShellIntegration::ShortcutLocations& creation_locations, 722 ShortcutCreationReason creation_reason) { 723 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 724 WebAppShortcutCreator shortcut_creator( 725 app_data_path, shortcut_info, base::mac::BaseBundleID()); 726 return shortcut_creator.CreateShortcuts(creation_reason); 727 } 728 729 void DeletePlatformShortcuts( 730 const base::FilePath& app_data_path, 731 const ShellIntegration::ShortcutInfo& shortcut_info) { 732 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 733 WebAppShortcutCreator shortcut_creator( 734 app_data_path, shortcut_info, base::mac::BaseBundleID()); 735 shortcut_creator.DeleteShortcuts(); 736 } 737 738 void UpdatePlatformShortcuts( 739 const base::FilePath& app_data_path, 740 const string16& old_app_title, 741 const ShellIntegration::ShortcutInfo& shortcut_info) { 742 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 743 WebAppShortcutCreator shortcut_creator( 744 app_data_path, shortcut_info, base::mac::BaseBundleID()); 745 shortcut_creator.UpdateShortcuts(); 746 } 747 748 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) { 749 const std::string profile_base_name = profile_path.BaseName().value(); 750 std::vector<base::FilePath> bundles = GetAllAppBundlesInPath( 751 profile_path.Append(chrome::kWebAppDirname), profile_base_name); 752 753 for (std::vector<base::FilePath>::const_iterator it = bundles.begin(); 754 it != bundles.end(); ++it) { 755 ShellIntegration::ShortcutInfo shortcut_info = 756 BuildShortcutInfoFromBundle(*it); 757 WebAppShortcutCreator shortcut_creator( 758 it->DirName(), shortcut_info, base::mac::BaseBundleID()); 759 shortcut_creator.DeleteShortcuts(); 760 } 761 } 762 763 } // namespace internals 764 765 } // namespace web_app 766