Home | History | Annotate | Download | only in web_applications
      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