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 "base/command_line.h" 11 #include "base/files/file_enumerator.h" 12 #include "base/files/file_util.h" 13 #include "base/files/scoped_temp_dir.h" 14 #include "base/mac/foundation_util.h" 15 #include "base/mac/launch_services_util.h" 16 #include "base/mac/mac_util.h" 17 #include "base/mac/scoped_cftyperef.h" 18 #include "base/mac/scoped_nsobject.h" 19 #include "base/metrics/sparse_histogram.h" 20 #include "base/path_service.h" 21 #include "base/process/process_handle.h" 22 #include "base/strings/string16.h" 23 #include "base/strings/string_number_conversions.h" 24 #include "base/strings/string_split.h" 25 #include "base/strings/string_util.h" 26 #include "base/strings/sys_string_conversions.h" 27 #include "base/strings/utf_string_conversions.h" 28 #include "base/version.h" 29 #include "chrome/browser/browser_process.h" 30 #import "chrome/browser/mac/dock.h" 31 #include "chrome/browser/profiles/profile.h" 32 #include "chrome/browser/profiles/profile_manager.h" 33 #include "chrome/browser/shell_integration.h" 34 #include "chrome/browser/ui/app_list/app_list_service.h" 35 #include "chrome/common/chrome_constants.h" 36 #include "chrome/common/chrome_paths.h" 37 #include "chrome/common/chrome_switches.h" 38 #include "chrome/common/chrome_version_info.h" 39 #import "chrome/common/mac/app_mode_common.h" 40 #include "chrome/grit/generated_resources.h" 41 #include "components/crx_file/id_util.h" 42 #include "content/public/browser/browser_thread.h" 43 #include "extensions/browser/extension_registry.h" 44 #include "extensions/common/extension.h" 45 #include "grit/chrome_unscaled_resources.h" 46 #import "skia/ext/skia_utils_mac.h" 47 #include "third_party/skia/include/core/SkBitmap.h" 48 #include "third_party/skia/include/core/SkColor.h" 49 #include "ui/base/l10n/l10n_util.h" 50 #import "ui/base/l10n/l10n_util_mac.h" 51 #include "ui/base/resource/resource_bundle.h" 52 #include "ui/gfx/image/image_family.h" 53 54 bool g_app_shims_allow_update_and_launch_in_tests = false; 55 56 namespace { 57 58 // Launch Services Key to run as an agent app, which doesn't launch in the dock. 59 NSString* const kLSUIElement = @"LSUIElement"; 60 61 class ScopedCarbonHandle { 62 public: 63 ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) { 64 DCHECK(handle_); 65 DCHECK_EQ(noErr, MemError()); 66 } 67 ~ScopedCarbonHandle() { DisposeHandle(handle_); } 68 69 Handle Get() { return handle_; } 70 char* Data() { return *handle_; } 71 size_t HandleSize() const { return GetHandleSize(handle_); } 72 73 IconFamilyHandle GetAsIconFamilyHandle() { 74 return reinterpret_cast<IconFamilyHandle>(handle_); 75 } 76 77 bool WriteDataToFile(const base::FilePath& path) { 78 NSData* data = [NSData dataWithBytes:Data() 79 length:HandleSize()]; 80 return [data writeToFile:base::mac::FilePathToNSString(path) 81 atomically:NO]; 82 } 83 84 private: 85 Handle handle_; 86 }; 87 88 void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) { 89 CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize()); 90 91 char* argb = handle->Data(); 92 SkAutoLockPixels lock(bitmap); 93 for (int y = 0; y < bitmap.height(); ++y) { 94 for (int x = 0; x < bitmap.width(); ++x) { 95 SkColor pixel = bitmap.getColor(x, y); 96 argb[0] = SkColorGetA(pixel); 97 argb[1] = SkColorGetR(pixel); 98 argb[2] = SkColorGetG(pixel); 99 argb[3] = SkColorGetB(pixel); 100 argb += 4; 101 } 102 } 103 } 104 105 // Adds |image| to |icon_family|. Returns true on success, false on failure. 106 bool AddGfxImageToIconFamily(IconFamilyHandle icon_family, 107 const gfx::Image& image) { 108 // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will 109 // have all the representations desired here for mac, from the kDesiredSizes 110 // array in web_app.cc. 111 SkBitmap bitmap = image.AsBitmap(); 112 if (bitmap.colorType() != kN32_SkColorType || 113 bitmap.width() != bitmap.height()) { 114 return false; 115 } 116 117 OSType icon_type; 118 switch (bitmap.width()) { 119 case 512: 120 icon_type = kIconServices512PixelDataARGB; 121 break; 122 case 256: 123 icon_type = kIconServices256PixelDataARGB; 124 break; 125 case 128: 126 icon_type = kIconServices128PixelDataARGB; 127 break; 128 case 48: 129 icon_type = kIconServices48PixelDataARGB; 130 break; 131 case 32: 132 icon_type = kIconServices32PixelDataARGB; 133 break; 134 case 16: 135 icon_type = kIconServices16PixelDataARGB; 136 break; 137 default: 138 return false; 139 } 140 141 ScopedCarbonHandle raw_data(bitmap.getSize()); 142 ConvertSkiaToARGB(bitmap, &raw_data); 143 OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get()); 144 DCHECK_EQ(noErr, result); 145 return result == noErr; 146 } 147 148 bool AppShimsDisabledForTest() { 149 // Disable app shims in tests because shims created in ~/Applications will not 150 // be cleaned up. 151 return CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType); 152 } 153 154 base::FilePath GetWritableApplicationsDirectory() { 155 base::FilePath path; 156 if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) { 157 if (!base::DirectoryExists(path)) { 158 if (!base::CreateDirectory(path)) 159 return base::FilePath(); 160 161 // Create a zero-byte ".localized" file to inherit localizations from OSX 162 // for folders that have special meaning. 163 base::WriteFile(path.Append(".localized"), NULL, 0); 164 } 165 return base::PathIsWritable(path) ? path : base::FilePath(); 166 } 167 return base::FilePath(); 168 } 169 170 // Given the path to an app bundle, return the resources directory. 171 base::FilePath GetResourcesPath(const base::FilePath& app_path) { 172 return app_path.Append("Contents").Append("Resources"); 173 } 174 175 bool HasExistingExtensionShim(const base::FilePath& destination_directory, 176 const std::string& extension_id, 177 const base::FilePath& own_basename) { 178 // Check if there any any other shims for the same extension. 179 base::FileEnumerator enumerator(destination_directory, 180 false /* recursive */, 181 base::FileEnumerator::DIRECTORIES); 182 for (base::FilePath shim_path = enumerator.Next(); 183 !shim_path.empty(); shim_path = enumerator.Next()) { 184 if (shim_path.BaseName() != own_basename && 185 EndsWith(shim_path.RemoveExtension().value(), 186 extension_id, 187 true /* case_sensitive */)) { 188 return true; 189 } 190 } 191 192 return false; 193 } 194 195 // Given the path to an app bundle, return the path to the Info.plist file. 196 NSString* GetPlistPath(const base::FilePath& bundle_path) { 197 return base::mac::FilePathToNSString( 198 bundle_path.Append("Contents").Append("Info.plist")); 199 } 200 201 NSMutableDictionary* ReadPlist(NSString* plist_path) { 202 return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path]; 203 } 204 205 // Takes the path to an app bundle and checks that the CrAppModeUserDataDir in 206 // the Info.plist starts with the current user_data_dir. This uses starts with 207 // instead of equals because the CrAppModeUserDataDir could be the user_data_dir 208 // or the |app_data_dir_|. 209 bool HasSameUserDataDir(const base::FilePath& bundle_path) { 210 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); 211 base::FilePath user_data_dir; 212 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); 213 DCHECK(!user_data_dir.empty()); 214 return StartsWithASCII( 215 base::SysNSStringToUTF8( 216 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]), 217 user_data_dir.value(), 218 true /* case_sensitive */); 219 } 220 221 void LaunchShimOnFileThread(const web_app::ShortcutInfo& shortcut_info, 222 bool launched_after_rebuild) { 223 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 224 base::FilePath shim_path = web_app::GetAppInstallPath(shortcut_info); 225 226 if (shim_path.empty() || 227 !base::PathExists(shim_path) || 228 !HasSameUserDataDir(shim_path)) { 229 // The user may have deleted the copy in the Applications folder, use the 230 // one in the web app's |app_data_dir_|. 231 base::FilePath app_data_dir = web_app::GetWebAppDataDirectory( 232 shortcut_info.profile_path, shortcut_info.extension_id, GURL()); 233 shim_path = app_data_dir.Append(shim_path.BaseName()); 234 } 235 236 if (!base::PathExists(shim_path)) 237 return; 238 239 CommandLine command_line(CommandLine::NO_PROGRAM); 240 command_line.AppendSwitchASCII( 241 app_mode::kLaunchedByChromeProcessId, 242 base::IntToString(base::GetCurrentProcId())); 243 if (launched_after_rebuild) 244 command_line.AppendSwitch(app_mode::kLaunchedAfterRebuild); 245 // Launch without activating (kLSLaunchDontSwitch). 246 base::mac::OpenApplicationWithPath( 247 shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL); 248 } 249 250 base::FilePath GetAppLoaderPath() { 251 return base::mac::PathForFrameworkBundleResource( 252 base::mac::NSToCFCast(@"app_mode_loader.app")); 253 } 254 255 void UpdateAndLaunchShimOnFileThread( 256 const web_app::ShortcutInfo& shortcut_info, 257 const extensions::FileHandlersInfo& file_handlers_info) { 258 base::FilePath shortcut_data_dir = web_app::GetWebAppDataDirectory( 259 shortcut_info.profile_path, shortcut_info.extension_id, GURL()); 260 web_app::internals::UpdatePlatformShortcuts( 261 shortcut_data_dir, base::string16(), shortcut_info, file_handlers_info); 262 LaunchShimOnFileThread(shortcut_info, true); 263 } 264 265 void UpdateAndLaunchShim( 266 const web_app::ShortcutInfo& shortcut_info, 267 const extensions::FileHandlersInfo& file_handlers_info) { 268 content::BrowserThread::PostTask( 269 content::BrowserThread::FILE, 270 FROM_HERE, 271 base::Bind( 272 &UpdateAndLaunchShimOnFileThread, shortcut_info, file_handlers_info)); 273 } 274 275 void RebuildAppAndLaunch(const web_app::ShortcutInfo& shortcut_info) { 276 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 277 if (shortcut_info.extension_id == app_mode::kAppListModeId) { 278 AppListService* app_list_service = 279 AppListService::Get(chrome::HOST_DESKTOP_TYPE_NATIVE); 280 app_list_service->CreateShortcut(); 281 app_list_service->Show(); 282 return; 283 } 284 285 ProfileManager* profile_manager = g_browser_process->profile_manager(); 286 Profile* profile = 287 profile_manager->GetProfileByPath(shortcut_info.profile_path); 288 if (!profile || !profile_manager->IsValidProfile(profile)) 289 return; 290 291 extensions::ExtensionRegistry* registry = 292 extensions::ExtensionRegistry::Get(profile); 293 const extensions::Extension* extension = registry->GetExtensionById( 294 shortcut_info.extension_id, extensions::ExtensionRegistry::ENABLED); 295 if (!extension || !extension->is_platform_app()) 296 return; 297 298 web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim)); 299 } 300 301 base::FilePath GetLocalizableAppShortcutsSubdirName() { 302 static const char kChromiumAppDirName[] = "Chromium Apps.localized"; 303 static const char kChromeAppDirName[] = "Chrome Apps.localized"; 304 static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized"; 305 306 switch (chrome::VersionInfo::GetChannel()) { 307 case chrome::VersionInfo::CHANNEL_UNKNOWN: 308 return base::FilePath(kChromiumAppDirName); 309 310 case chrome::VersionInfo::CHANNEL_CANARY: 311 return base::FilePath(kChromeCanaryAppDirName); 312 313 default: 314 return base::FilePath(kChromeAppDirName); 315 } 316 } 317 318 // Creates a canvas the same size as |overlay|, copies the appropriate 319 // representation from |backgound| into it (according to Cocoa), then draws 320 // |overlay| over it using NSCompositeSourceOver. 321 NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) { 322 DCHECK(background); 323 NSInteger dimension = [overlay pixelsWide]; 324 DCHECK_EQ(dimension, [overlay pixelsHigh]); 325 base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc] 326 initWithBitmapDataPlanes:NULL 327 pixelsWide:dimension 328 pixelsHigh:dimension 329 bitsPerSample:8 330 samplesPerPixel:4 331 hasAlpha:YES 332 isPlanar:NO 333 colorSpaceName:NSCalibratedRGBColorSpace 334 bytesPerRow:0 335 bitsPerPixel:0]); 336 337 // There isn't a colorspace name constant for sRGB, so retag. 338 NSBitmapImageRep* srgb_canvas = [canvas 339 bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]]; 340 canvas.reset([srgb_canvas retain]); 341 342 // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI. 343 [canvas setSize:NSMakeSize(dimension, dimension)]; 344 345 NSGraphicsContext* drawing_context = 346 [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas]; 347 [NSGraphicsContext saveGraphicsState]; 348 [NSGraphicsContext setCurrentContext:drawing_context]; 349 [background drawInRect:NSMakeRect(0, 0, dimension, dimension) 350 fromRect:NSZeroRect 351 operation:NSCompositeCopy 352 fraction:1.0]; 353 [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension) 354 fromRect:NSZeroRect 355 operation:NSCompositeSourceOver 356 fraction:1.0 357 respectFlipped:NO 358 hints:0]; 359 [NSGraphicsContext restoreGraphicsState]; 360 return canvas.autorelease(); 361 } 362 363 // Helper function to extract the single NSImageRep held in a resource bundle 364 // image. 365 NSImageRep* ImageRepForResource(int resource_id) { 366 gfx::Image& image = 367 ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id); 368 NSArray* image_reps = [image.AsNSImage() representations]; 369 DCHECK_EQ(1u, [image_reps count]); 370 return [image_reps objectAtIndex:0]; 371 } 372 373 // Adds a localized strings file for the Chrome Apps directory using the current 374 // locale. OSX will use this for the display name. 375 // + Chrome Apps.localized (|apps_directory|) 376 // | + .localized 377 // | | en.strings 378 // | | de.strings 379 void UpdateAppShortcutsSubdirLocalizedName( 380 const base::FilePath& apps_directory) { 381 base::FilePath localized = apps_directory.Append(".localized"); 382 if (!base::CreateDirectory(localized)) 383 return; 384 385 base::FilePath directory_name = apps_directory.BaseName().RemoveExtension(); 386 base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName(); 387 NSDictionary* strings_dict = @{ 388 base::mac::FilePathToNSString(directory_name) : 389 base::SysUTF16ToNSString(localized_name) 390 }; 391 392 std::string locale = l10n_util::NormalizeLocale( 393 l10n_util::GetApplicationLocale(std::string())); 394 395 NSString* strings_path = base::mac::FilePathToNSString( 396 localized.Append(locale + ".strings")); 397 [strings_dict writeToFile:strings_path 398 atomically:YES]; 399 400 base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]); 401 402 // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a 403 // bug when dealing with named NSImages where it incorrectly handles alpha 404 // premultiplication. This is most noticable with small assets since the 1px 405 // border is a much larger component of the small icons. 406 // See http://crbug.com/305373 for details. 407 [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)]; 408 [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)]; 409 410 // Brand larger folder assets with an embossed app launcher logo to conserve 411 // distro size and for better consistency with changing hue across OSX 412 // versions. The folder is textured, so compresses poorly without this. 413 const int kBrandResourceIds[] = { 414 IDR_APPS_FOLDER_OVERLAY_128, 415 IDR_APPS_FOLDER_OVERLAY_512, 416 }; 417 NSImage* base_image = [NSImage imageNamed:NSImageNameFolder]; 418 for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) { 419 NSImageRep* with_overlay = 420 OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i])); 421 DCHECK(with_overlay); 422 if (with_overlay) 423 [folder_icon_image addRepresentation:with_overlay]; 424 } 425 [[NSWorkspace sharedWorkspace] 426 setIcon:folder_icon_image 427 forFile:base::mac::FilePathToNSString(apps_directory) 428 options:0]; 429 } 430 431 void DeletePathAndParentIfEmpty(const base::FilePath& app_path) { 432 DCHECK(!app_path.empty()); 433 base::DeleteFile(app_path, true); 434 base::FilePath apps_folder = app_path.DirName(); 435 if (base::IsDirectoryEmpty(apps_folder)) 436 base::DeleteFile(apps_folder, false); 437 } 438 439 bool IsShimForProfile(const base::FilePath& base_name, 440 const std::string& profile_base_name) { 441 if (!StartsWithASCII(base_name.value(), profile_base_name, true)) 442 return false; 443 444 if (base_name.Extension() != ".app") 445 return false; 446 447 std::string app_id = base_name.RemoveExtension().value(); 448 // Strip (profile_base_name + " ") from the start. 449 app_id = app_id.substr(profile_base_name.size() + 1); 450 return crx_file::id_util::IdIsValid(app_id); 451 } 452 453 std::vector<base::FilePath> GetAllAppBundlesInPath( 454 const base::FilePath& internal_shortcut_path, 455 const std::string& profile_base_name) { 456 std::vector<base::FilePath> bundle_paths; 457 458 base::FileEnumerator enumerator(internal_shortcut_path, 459 true /* recursive */, 460 base::FileEnumerator::DIRECTORIES); 461 for (base::FilePath bundle_path = enumerator.Next(); 462 !bundle_path.empty(); bundle_path = enumerator.Next()) { 463 if (IsShimForProfile(bundle_path.BaseName(), profile_base_name)) 464 bundle_paths.push_back(bundle_path); 465 } 466 467 return bundle_paths; 468 } 469 470 web_app::ShortcutInfo BuildShortcutInfoFromBundle( 471 const base::FilePath& bundle_path) { 472 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); 473 474 web_app::ShortcutInfo shortcut_info; 475 shortcut_info.extension_id = base::SysNSStringToUTF8( 476 [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]); 477 shortcut_info.is_platform_app = true; 478 shortcut_info.url = GURL(base::SysNSStringToUTF8( 479 [plist valueForKey:app_mode::kCrAppModeShortcutURLKey])); 480 shortcut_info.title = base::SysNSStringToUTF16( 481 [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]); 482 shortcut_info.profile_name = base::SysNSStringToUTF8( 483 [plist valueForKey:app_mode::kCrAppModeProfileNameKey]); 484 485 // Figure out the profile_path. Since the user_data_dir could contain the 486 // path to the web app data dir. 487 base::FilePath user_data_dir = base::mac::NSStringToFilePath( 488 [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]); 489 base::FilePath profile_base_name = base::mac::NSStringToFilePath( 490 [plist valueForKey:app_mode::kCrAppModeProfileDirKey]); 491 if (user_data_dir.DirName().DirName().BaseName() == profile_base_name) 492 shortcut_info.profile_path = user_data_dir.DirName().DirName(); 493 else 494 shortcut_info.profile_path = user_data_dir.Append(profile_base_name); 495 496 return shortcut_info; 497 } 498 499 web_app::ShortcutInfo RecordAppShimErrorAndBuildShortcutInfo( 500 const base::FilePath& bundle_path) { 501 NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path)); 502 base::Version full_version(base::SysNSStringToUTF8( 503 [plist valueForKey:app_mode::kCFBundleShortVersionStringKey])); 504 int major_version = 0; 505 if (full_version.IsValid()) 506 major_version = full_version.components()[0]; 507 UMA_HISTOGRAM_SPARSE_SLOWLY("Apps.AppShimErrorVersion", major_version); 508 509 return BuildShortcutInfoFromBundle(bundle_path); 510 } 511 512 void UpdateFileTypes(NSMutableDictionary* plist, 513 const extensions::FileHandlersInfo& file_handlers_info) { 514 NSMutableArray* document_types = 515 [NSMutableArray arrayWithCapacity:file_handlers_info.size()]; 516 517 for (extensions::FileHandlersInfo::const_iterator info_it = 518 file_handlers_info.begin(); 519 info_it != file_handlers_info.end(); 520 ++info_it) { 521 const extensions::FileHandlerInfo& info = *info_it; 522 523 NSMutableArray* file_extensions = 524 [NSMutableArray arrayWithCapacity:info.extensions.size()]; 525 for (std::set<std::string>::iterator it = info.extensions.begin(); 526 it != info.extensions.end(); 527 ++it) { 528 [file_extensions addObject:base::SysUTF8ToNSString(*it)]; 529 } 530 531 NSMutableArray* mime_types = 532 [NSMutableArray arrayWithCapacity:info.types.size()]; 533 for (std::set<std::string>::iterator it = info.types.begin(); 534 it != info.types.end(); 535 ++it) { 536 [mime_types addObject:base::SysUTF8ToNSString(*it)]; 537 } 538 539 NSDictionary* type_dictionary = @{ 540 // TODO(jackhou): Add the type name and and icon file once the manifest 541 // supports these. 542 // app_mode::kCFBundleTypeNameKey : , 543 // app_mode::kCFBundleTypeIconFileKey : , 544 app_mode::kCFBundleTypeExtensionsKey : file_extensions, 545 app_mode::kCFBundleTypeMIMETypesKey : mime_types, 546 app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer 547 }; 548 [document_types addObject:type_dictionary]; 549 } 550 551 [plist setObject:document_types 552 forKey:app_mode::kCFBundleDocumentTypesKey]; 553 } 554 555 } // namespace 556 557 @interface CrCreateAppShortcutCheckboxObserver : NSObject { 558 @private 559 NSButton* checkbox_; 560 NSButton* continueButton_; 561 } 562 563 - (id)initWithCheckbox:(NSButton*)checkbox 564 continueButton:(NSButton*)continueButton; 565 - (void)startObserving; 566 - (void)stopObserving; 567 @end 568 569 @implementation CrCreateAppShortcutCheckboxObserver 570 571 - (id)initWithCheckbox:(NSButton*)checkbox 572 continueButton:(NSButton*)continueButton { 573 if ((self = [super init])) { 574 checkbox_ = checkbox; 575 continueButton_ = continueButton; 576 } 577 return self; 578 } 579 580 - (void)startObserving { 581 [checkbox_ addObserver:self 582 forKeyPath:@"cell.state" 583 options:0 584 context:nil]; 585 } 586 587 - (void)stopObserving { 588 [checkbox_ removeObserver:self 589 forKeyPath:@"cell.state"]; 590 } 591 592 - (void)observeValueForKeyPath:(NSString*)keyPath 593 ofObject:(id)object 594 change:(NSDictionary*)change 595 context:(void*)context { 596 [continueButton_ setEnabled:([checkbox_ state] == NSOnState)]; 597 } 598 599 @end 600 601 namespace web_app { 602 603 WebAppShortcutCreator::WebAppShortcutCreator( 604 const base::FilePath& app_data_dir, 605 const ShortcutInfo& shortcut_info, 606 const extensions::FileHandlersInfo& file_handlers_info) 607 : app_data_dir_(app_data_dir), 608 info_(shortcut_info), 609 file_handlers_info_(file_handlers_info) {} 610 611 WebAppShortcutCreator::~WebAppShortcutCreator() {} 612 613 base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const { 614 base::FilePath applications_dir = GetApplicationsDirname(); 615 return applications_dir.empty() ? 616 base::FilePath() : applications_dir.Append(GetShortcutBasename()); 617 } 618 619 base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const { 620 return app_data_dir_.Append(GetShortcutBasename()); 621 } 622 623 base::FilePath WebAppShortcutCreator::GetShortcutBasename() const { 624 std::string app_name; 625 // Check if there should be a separate shortcut made for different profiles. 626 // Such shortcuts will have a |profile_name| set on the ShortcutInfo, 627 // otherwise it will be empty. 628 if (!info_.profile_name.empty()) { 629 app_name += info_.profile_path.BaseName().value(); 630 app_name += ' '; 631 } 632 app_name += info_.extension_id; 633 return base::FilePath(app_name).ReplaceExtension("app"); 634 } 635 636 bool WebAppShortcutCreator::BuildShortcut( 637 const base::FilePath& staging_path) const { 638 // Update the app's plist and icon in a temp directory. This works around 639 // a Finder bug where the app's icon doesn't properly update. 640 if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) { 641 LOG(ERROR) << "Copying app to staging path: " << staging_path.value() 642 << " failed."; 643 return false; 644 } 645 646 return UpdatePlist(staging_path) && 647 UpdateDisplayName(staging_path) && 648 UpdateIcon(staging_path); 649 } 650 651 size_t WebAppShortcutCreator::CreateShortcutsIn( 652 const std::vector<base::FilePath>& folders) const { 653 size_t succeeded = 0; 654 655 base::ScopedTempDir scoped_temp_dir; 656 if (!scoped_temp_dir.CreateUniqueTempDir()) 657 return 0; 658 659 base::FilePath app_name = GetShortcutBasename(); 660 base::FilePath staging_path = scoped_temp_dir.path().Append(app_name); 661 if (!BuildShortcut(staging_path)) 662 return 0; 663 664 for (std::vector<base::FilePath>::const_iterator it = folders.begin(); 665 it != folders.end(); ++it) { 666 const base::FilePath& dst_path = *it; 667 if (!base::CreateDirectory(dst_path)) { 668 LOG(ERROR) << "Creating directory " << dst_path.value() << " failed."; 669 return succeeded; 670 } 671 672 if (!base::CopyDirectory(staging_path, dst_path, true)) { 673 LOG(ERROR) << "Copying app to dst path: " << dst_path.value() 674 << " failed"; 675 return succeeded; 676 } 677 678 // Remove the quarantine attribute from both the bundle and the executable. 679 base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name)); 680 base::mac::RemoveQuarantineAttribute( 681 dst_path.Append(app_name) 682 .Append("Contents").Append("MacOS").Append("app_mode_loader")); 683 ++succeeded; 684 } 685 686 return succeeded; 687 } 688 689 bool WebAppShortcutCreator::CreateShortcuts( 690 ShortcutCreationReason creation_reason, 691 ShortcutLocations creation_locations) { 692 const base::FilePath applications_dir = GetApplicationsDirname(); 693 if (applications_dir.empty() || 694 !base::DirectoryExists(applications_dir.DirName())) { 695 LOG(ERROR) << "Couldn't find an Applications directory to copy app to."; 696 return false; 697 } 698 699 UpdateAppShortcutsSubdirLocalizedName(applications_dir); 700 701 // If non-nil, this path is added to the OSX Dock after creating shortcuts. 702 NSString* path_to_add_to_dock = nil; 703 704 std::vector<base::FilePath> paths; 705 706 // The app list shim is not tied to a particular profile, so omit the copy 707 // placed under the profile path. For shims, this copy is used when the 708 // version under Applications is removed, and not needed for app list because 709 // setting LSUIElement means there is no Dock "running" status to show. 710 const bool is_app_list = info_.extension_id == app_mode::kAppListModeId; 711 if (is_app_list) { 712 path_to_add_to_dock = base::SysUTF8ToNSString( 713 applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe()); 714 } else { 715 paths.push_back(app_data_dir_); 716 } 717 718 bool shortcut_visible = 719 creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN; 720 if (shortcut_visible) 721 paths.push_back(applications_dir); 722 723 DCHECK(!paths.empty()); 724 size_t success_count = CreateShortcutsIn(paths); 725 if (success_count == 0) 726 return false; 727 728 if (!is_app_list) 729 UpdateInternalBundleIdentifier(); 730 731 if (success_count != paths.size()) 732 return false; 733 734 if (creation_locations.in_quick_launch_bar && path_to_add_to_dock && 735 shortcut_visible) { 736 switch (dock::AddIcon(path_to_add_to_dock, nil)) { 737 case dock::IconAddFailure: 738 // If adding the icon failed, instead reveal the Finder window. 739 RevealAppShimInFinder(); 740 break; 741 case dock::IconAddSuccess: 742 case dock::IconAlreadyPresent: 743 break; 744 } 745 return true; 746 } 747 748 if (creation_reason == SHORTCUT_CREATION_BY_USER) 749 RevealAppShimInFinder(); 750 751 return true; 752 } 753 754 void WebAppShortcutCreator::DeleteShortcuts() { 755 base::FilePath app_path = GetApplicationsShortcutPath(); 756 if (!app_path.empty() && HasSameUserDataDir(app_path)) 757 DeletePathAndParentIfEmpty(app_path); 758 759 // In case the user has moved/renamed/copied the app bundle. 760 base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier()); 761 if (!bundle_path.empty() && HasSameUserDataDir(bundle_path)) 762 base::DeleteFile(bundle_path, true); 763 764 // Delete the internal one. 765 DeletePathAndParentIfEmpty(GetInternalShortcutPath()); 766 } 767 768 bool WebAppShortcutCreator::UpdateShortcuts() { 769 std::vector<base::FilePath> paths; 770 base::DeleteFile(GetInternalShortcutPath(), true); 771 paths.push_back(app_data_dir_); 772 773 // Try to update the copy under /Applications. If that does not exist, check 774 // if a matching bundle can be found elsewhere. 775 base::FilePath app_path = GetApplicationsShortcutPath(); 776 if (app_path.empty() || !base::PathExists(app_path)) 777 app_path = GetAppBundleById(GetBundleIdentifier()); 778 779 if (!app_path.empty()) { 780 base::DeleteFile(app_path, true); 781 paths.push_back(app_path.DirName()); 782 } 783 784 size_t success_count = CreateShortcutsIn(paths); 785 if (success_count == 0) 786 return false; 787 788 UpdateInternalBundleIdentifier(); 789 return success_count == paths.size() && !app_path.empty(); 790 } 791 792 base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const { 793 base::FilePath path = GetWritableApplicationsDirectory(); 794 if (path.empty()) 795 return path; 796 797 return path.Append(GetLocalizableAppShortcutsSubdirName()); 798 } 799 800 bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const { 801 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id); 802 NSString* extension_title = base::SysUTF16ToNSString(info_.title); 803 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec()); 804 NSString* chrome_bundle_id = 805 base::SysUTF8ToNSString(base::mac::BaseBundleID()); 806 NSDictionary* replacement_dict = 807 [NSDictionary dictionaryWithObjectsAndKeys: 808 extension_id, app_mode::kShortcutIdPlaceholder, 809 extension_title, app_mode::kShortcutNamePlaceholder, 810 extension_url, app_mode::kShortcutURLPlaceholder, 811 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder, 812 nil]; 813 814 NSString* plist_path = GetPlistPath(app_path); 815 NSMutableDictionary* plist = ReadPlist(plist_path); 816 NSArray* keys = [plist allKeys]; 817 818 // 1. Fill in variables. 819 for (id key in keys) { 820 NSString* value = [plist valueForKey:key]; 821 if (![value isKindOfClass:[NSString class]] || [value length] < 2) 822 continue; 823 824 // Remove leading and trailing '@'s. 825 NSString* variable = 826 [value substringWithRange:NSMakeRange(1, [value length] - 2)]; 827 828 NSString* substitution = [replacement_dict valueForKey:variable]; 829 if (substitution) 830 [plist setObject:substitution forKey:key]; 831 } 832 833 // 2. Fill in other values. 834 [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier()) 835 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; 836 [plist setObject:base::mac::FilePathToNSString(app_data_dir_) 837 forKey:app_mode::kCrAppModeUserDataDirKey]; 838 [plist setObject:base::mac::FilePathToNSString(info_.profile_path.BaseName()) 839 forKey:app_mode::kCrAppModeProfileDirKey]; 840 [plist setObject:base::SysUTF8ToNSString(info_.profile_name) 841 forKey:app_mode::kCrAppModeProfileNameKey]; 842 [plist setObject:[NSNumber numberWithBool:YES] 843 forKey:app_mode::kLSHasLocalizedDisplayNameKey]; 844 if (info_.extension_id == app_mode::kAppListModeId) { 845 // Prevent the app list from bouncing in the dock, and getting a run light. 846 [plist setObject:[NSNumber numberWithBool:YES] 847 forKey:kLSUIElement]; 848 } 849 850 base::FilePath app_name = app_path.BaseName().RemoveExtension(); 851 [plist setObject:base::mac::FilePathToNSString(app_name) 852 forKey:base::mac::CFToNSCast(kCFBundleNameKey)]; 853 854 if (CommandLine::ForCurrentProcess()->HasSwitch( 855 switches::kEnableAppsFileAssociations)) { 856 UpdateFileTypes(plist, file_handlers_info_); 857 } 858 859 return [plist writeToFile:plist_path 860 atomically:YES]; 861 } 862 863 bool WebAppShortcutCreator::UpdateDisplayName( 864 const base::FilePath& app_path) const { 865 // OSX searches for the best language in the order of preferred languages. 866 // Since we only have one localization directory, it will choose this one. 867 base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj"); 868 if (!base::CreateDirectory(localized_dir)) 869 return false; 870 871 NSString* bundle_name = base::SysUTF16ToNSString(info_.title); 872 NSString* display_name = base::SysUTF16ToNSString(info_.title); 873 if (HasExistingExtensionShim(GetApplicationsDirname(), 874 info_.extension_id, 875 app_path.BaseName())) { 876 display_name = [bundle_name 877 stringByAppendingString:base::SysUTF8ToNSString( 878 " (" + info_.profile_name + ")")]; 879 } 880 881 NSDictionary* strings_plist = @{ 882 base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name, 883 app_mode::kCFBundleDisplayNameKey : display_name 884 }; 885 886 NSString* localized_path = base::mac::FilePathToNSString( 887 localized_dir.Append("InfoPlist.strings")); 888 return [strings_plist writeToFile:localized_path 889 atomically:YES]; 890 } 891 892 bool WebAppShortcutCreator::UpdateIcon(const base::FilePath& app_path) const { 893 if (info_.favicon.empty()) 894 return true; 895 896 ScopedCarbonHandle icon_family(0); 897 bool image_added = false; 898 for (gfx::ImageFamily::const_iterator it = info_.favicon.begin(); 899 it != info_.favicon.end(); ++it) { 900 if (it->IsEmpty()) 901 continue; 902 903 // Missing an icon size is not fatal so don't fail if adding the bitmap 904 // doesn't work. 905 if (!AddGfxImageToIconFamily(icon_family.GetAsIconFamilyHandle(), *it)) 906 continue; 907 908 image_added = true; 909 } 910 911 if (!image_added) 912 return false; 913 914 base::FilePath resources_path = GetResourcesPath(app_path); 915 if (!base::CreateDirectory(resources_path)) 916 return false; 917 918 return icon_family.WriteDataToFile(resources_path.Append("app.icns")); 919 } 920 921 bool WebAppShortcutCreator::UpdateInternalBundleIdentifier() const { 922 NSString* plist_path = GetPlistPath(GetInternalShortcutPath()); 923 NSMutableDictionary* plist = ReadPlist(plist_path); 924 925 [plist setObject:base::SysUTF8ToNSString(GetInternalBundleIdentifier()) 926 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]; 927 return [plist writeToFile:plist_path 928 atomically:YES]; 929 } 930 931 base::FilePath WebAppShortcutCreator::GetAppBundleById( 932 const std::string& bundle_id) const { 933 base::ScopedCFTypeRef<CFStringRef> bundle_id_cf( 934 base::SysUTF8ToCFStringRef(bundle_id)); 935 CFURLRef url_ref = NULL; 936 OSStatus status = LSFindApplicationForInfo( 937 kLSUnknownCreator, bundle_id_cf.get(), NULL, NULL, &url_ref); 938 if (status != noErr) 939 return base::FilePath(); 940 941 base::ScopedCFTypeRef<CFURLRef> url(url_ref); 942 NSString* path_string = [base::mac::CFToNSCast(url.get()) path]; 943 return base::FilePath([path_string fileSystemRepresentation]); 944 } 945 946 std::string WebAppShortcutCreator::GetBundleIdentifier() const { 947 // Replace spaces in the profile path with hyphen. 948 std::string normalized_profile_path; 949 base::ReplaceChars(info_.profile_path.BaseName().value(), 950 " ", "-", &normalized_profile_path); 951 952 // This matches APP_MODE_APP_BUNDLE_ID in chrome/chrome.gyp. 953 std::string bundle_id = 954 base::mac::BaseBundleID() + std::string(".app.") + 955 normalized_profile_path + "-" + info_.extension_id; 956 957 return bundle_id; 958 } 959 960 std::string WebAppShortcutCreator::GetInternalBundleIdentifier() const { 961 return GetBundleIdentifier() + "-internal"; 962 } 963 964 void WebAppShortcutCreator::RevealAppShimInFinder() const { 965 base::FilePath app_path = GetApplicationsShortcutPath(); 966 if (app_path.empty()) 967 return; 968 969 [[NSWorkspace sharedWorkspace] 970 selectFile:base::mac::FilePathToNSString(app_path) 971 inFileViewerRootedAtPath:nil]; 972 } 973 974 base::FilePath GetAppInstallPath(const ShortcutInfo& shortcut_info) { 975 WebAppShortcutCreator shortcut_creator( 976 base::FilePath(), shortcut_info, extensions::FileHandlersInfo()); 977 return shortcut_creator.GetApplicationsShortcutPath(); 978 } 979 980 void MaybeLaunchShortcut(const ShortcutInfo& shortcut_info) { 981 if (AppShimsDisabledForTest() && 982 !g_app_shims_allow_update_and_launch_in_tests) { 983 return; 984 } 985 986 content::BrowserThread::PostTask( 987 content::BrowserThread::FILE, 988 FROM_HERE, 989 base::Bind(&LaunchShimOnFileThread, shortcut_info, false)); 990 } 991 992 bool MaybeRebuildShortcut(const CommandLine& command_line) { 993 if (!command_line.HasSwitch(app_mode::kAppShimError)) 994 return false; 995 996 base::PostTaskAndReplyWithResult( 997 content::BrowserThread::GetBlockingPool(), 998 FROM_HERE, 999 base::Bind(&RecordAppShimErrorAndBuildShortcutInfo, 1000 command_line.GetSwitchValuePath(app_mode::kAppShimError)), 1001 base::Bind(&RebuildAppAndLaunch)); 1002 return true; 1003 } 1004 1005 // Called when the app's ShortcutInfo (with icon) is loaded when creating app 1006 // shortcuts. 1007 void CreateAppShortcutInfoLoaded( 1008 Profile* profile, 1009 const extensions::Extension* app, 1010 const base::Callback<void(bool)>& close_callback, 1011 const ShortcutInfo& shortcut_info) { 1012 base::scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); 1013 1014 NSButton* continue_button = [alert 1015 addButtonWithTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_COMMIT)]; 1016 [continue_button setKeyEquivalent:@""]; 1017 1018 NSButton* cancel_button = 1019 [alert addButtonWithTitle:l10n_util::GetNSString(IDS_CANCEL)]; 1020 [cancel_button setKeyEquivalent:@"\r"]; 1021 1022 [alert setMessageText:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_LABEL)]; 1023 [alert setAlertStyle:NSInformationalAlertStyle]; 1024 1025 base::scoped_nsobject<NSButton> application_folder_checkbox( 1026 [[NSButton alloc] initWithFrame:NSZeroRect]); 1027 [application_folder_checkbox setButtonType:NSSwitchButton]; 1028 [application_folder_checkbox 1029 setTitle:l10n_util::GetNSString(IDS_CREATE_SHORTCUTS_APP_FOLDER_CHKBOX)]; 1030 [application_folder_checkbox setState:NSOnState]; 1031 [application_folder_checkbox sizeToFit]; 1032 1033 base::scoped_nsobject<CrCreateAppShortcutCheckboxObserver> checkbox_observer( 1034 [[CrCreateAppShortcutCheckboxObserver alloc] 1035 initWithCheckbox:application_folder_checkbox 1036 continueButton:continue_button]); 1037 [checkbox_observer startObserving]; 1038 1039 [alert setAccessoryView:application_folder_checkbox]; 1040 1041 const int kIconPreviewSizePixels = 128; 1042 const int kIconPreviewTargetSize = 64; 1043 const gfx::Image* icon = shortcut_info.favicon.GetBest( 1044 kIconPreviewSizePixels, kIconPreviewSizePixels); 1045 1046 if (icon && !icon->IsEmpty()) { 1047 NSImage* icon_image = icon->ToNSImage(); 1048 [icon_image 1049 setSize:NSMakeSize(kIconPreviewTargetSize, kIconPreviewTargetSize)]; 1050 [alert setIcon:icon_image]; 1051 } 1052 1053 bool dialog_accepted = false; 1054 if ([alert runModal] == NSAlertFirstButtonReturn && 1055 [application_folder_checkbox state] == NSOnState) { 1056 dialog_accepted = true; 1057 CreateShortcuts( 1058 SHORTCUT_CREATION_BY_USER, ShortcutLocations(), profile, app); 1059 } 1060 1061 [checkbox_observer stopObserving]; 1062 1063 if (!close_callback.is_null()) 1064 close_callback.Run(dialog_accepted); 1065 } 1066 1067 void UpdateShortcutsForAllApps(Profile* profile, 1068 const base::Closure& callback) { 1069 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 1070 1071 extensions::ExtensionRegistry* registry = 1072 extensions::ExtensionRegistry::Get(profile); 1073 if (!registry) 1074 return; 1075 1076 // Update all apps. 1077 scoped_ptr<extensions::ExtensionSet> everything = 1078 registry->GenerateInstalledExtensionsSet(); 1079 for (extensions::ExtensionSet::const_iterator it = everything->begin(); 1080 it != everything->end(); ++it) { 1081 if (web_app::ShouldCreateShortcutFor(profile, it->get())) 1082 web_app::UpdateAllShortcuts(base::string16(), profile, it->get()); 1083 } 1084 1085 callback.Run(); 1086 } 1087 1088 namespace internals { 1089 1090 bool CreatePlatformShortcuts( 1091 const base::FilePath& app_data_path, 1092 const ShortcutInfo& shortcut_info, 1093 const extensions::FileHandlersInfo& file_handlers_info, 1094 const ShortcutLocations& creation_locations, 1095 ShortcutCreationReason creation_reason) { 1096 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 1097 if (AppShimsDisabledForTest()) 1098 return true; 1099 1100 WebAppShortcutCreator shortcut_creator( 1101 app_data_path, shortcut_info, file_handlers_info); 1102 return shortcut_creator.CreateShortcuts(creation_reason, creation_locations); 1103 } 1104 1105 void DeletePlatformShortcuts(const base::FilePath& app_data_path, 1106 const ShortcutInfo& shortcut_info) { 1107 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 1108 WebAppShortcutCreator shortcut_creator( 1109 app_data_path, shortcut_info, extensions::FileHandlersInfo()); 1110 shortcut_creator.DeleteShortcuts(); 1111 } 1112 1113 void UpdatePlatformShortcuts( 1114 const base::FilePath& app_data_path, 1115 const base::string16& old_app_title, 1116 const ShortcutInfo& shortcut_info, 1117 const extensions::FileHandlersInfo& file_handlers_info) { 1118 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 1119 if (AppShimsDisabledForTest() && 1120 !g_app_shims_allow_update_and_launch_in_tests) { 1121 return; 1122 } 1123 1124 WebAppShortcutCreator shortcut_creator( 1125 app_data_path, shortcut_info, file_handlers_info); 1126 shortcut_creator.UpdateShortcuts(); 1127 } 1128 1129 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) { 1130 const std::string profile_base_name = profile_path.BaseName().value(); 1131 std::vector<base::FilePath> bundles = GetAllAppBundlesInPath( 1132 profile_path.Append(chrome::kWebAppDirname), profile_base_name); 1133 1134 for (std::vector<base::FilePath>::const_iterator it = bundles.begin(); 1135 it != bundles.end(); ++it) { 1136 web_app::ShortcutInfo shortcut_info = 1137 BuildShortcutInfoFromBundle(*it); 1138 WebAppShortcutCreator shortcut_creator( 1139 it->DirName(), shortcut_info, extensions::FileHandlersInfo()); 1140 shortcut_creator.DeleteShortcuts(); 1141 } 1142 } 1143 1144 } // namespace internals 1145 1146 } // namespace web_app 1147 1148 namespace chrome { 1149 1150 void ShowCreateChromeAppShortcutsDialog( 1151 gfx::NativeWindow /*parent_window*/, 1152 Profile* profile, 1153 const extensions::Extension* app, 1154 const base::Callback<void(bool)>& close_callback) { 1155 web_app::GetShortcutInfoForApp( 1156 app, 1157 profile, 1158 base::Bind(&web_app::CreateAppShortcutInfoLoaded, 1159 profile, 1160 app, 1161 close_callback)); 1162 } 1163 1164 } // namespace chrome 1165