<commit_before>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/web_applications/web_app_mac.h"

#import <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>

#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/sparse_histogram.h"
#include "base/path_service.h"
#include "base/process/process_handle.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#import "chrome/browser/mac/dock.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/ui/app_list/app_list_service.h"
#include "chrome/browser/ui/cocoa/key_equivalent_constants.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/chrome_version_info.h"
#import "chrome/common/mac/app_mode_common.h"
#include "chrome/grit/generated_resources.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "grit/chrome_unscaled_resources.h"
#import "skia/ext/skia_utils_mac.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_family.h"

bool g_app_shims_allow_update_and_launch_in_tests = false;

namespace {

// Launch Services Key to run as an agent app, which doesn't launch in the dock.
NSString* const kLSUIElement = @"LSUIElement";

class ScopedCarbonHandle {
 public:
  ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
    DCHECK(handle_);
    DCHECK_EQ(noErr, MemError());
  }
  ~ScopedCarbonHandle() { DisposeHandle(handle_); }

  Handle Get() { return handle_; }
  char* Data() { return *handle_; }
  size_t HandleSize() const { return GetHandleSize(handle_); }

  IconFamilyHandle GetAsIconFamilyHandle() {
    return reinterpret_cast<IconFamilyHandle>(handle_);
  }

  bool WriteDataToFile(const base::FilePath& path) {
    NSData* data = [NSData dataWithBytes:Data()
                                  length:HandleSize()];
    return [data writeToFile:base::mac::FilePathToNSString(path)
                  atomically:NO];
  }

 private:
  Handle handle_;
};

void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
  CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());

  char* argb = handle->Data();
  SkAutoLockPixels lock(bitmap);
  for (int y = 0; y < bitmap.height(); ++y) {
    for (int x = 0; x < bitmap.width(); ++x) {
      SkColor pixel = bitmap.getColor(x, y);
      argb[0] = SkColorGetA(pixel);
      argb[1] = SkColorGetR(pixel);
      argb[2] = SkColorGetG(pixel);
      argb[3] = SkColorGetB(pixel);
      argb += 4;
    }
  }
}

// Adds |image| to |icon_family|. Returns true on success, false on failure.
bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
                             const gfx::Image& image) {
  // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
  // have all the representations desired here for mac, from the kDesiredSizes
  // array in web_app.cc.
  SkBitmap bitmap = image.AsBitmap();
  if (bitmap.colorType() != kN32_SkColorType ||
      bitmap.width() != bitmap.height()) {
    return false;
  }

  OSType icon_type;
  switch (bitmap.width()) {
    case 512:
      icon_type = kIconServices512PixelDataARGB;
      break;
    case 256:
      icon_type = kIconServices256PixelDataARGB;
      break;
    case 128:
      icon_type = kIconServices128PixelDataARGB;
      break;
    case 48:
      icon_type = kIconServices48PixelDataARGB;
      break;
    case 32:
      icon_type = kIconServices32PixelDataARGB;
      break;
    case 16:
      icon_type = kIconServices16PixelDataARGB;
      break;
    default:
      return false;
  }

  ScopedCarbonHandle raw_data(bitmap.getSize());
  ConvertSkiaToARGB(bitmap, &raw_data);
  OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
  DCHECK_EQ(noErr, result);
  return result == noErr;
}

bool AppShimsDisabledForTest() {
  // Disable app shims in tests because shims created in ~/Applications will not
  // be cleaned up.
  return base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType);
}

base::FilePath GetWritableApplicationsDirectory() {
  base::FilePath path;
  if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
    if (!base::DirectoryExists(path)) {
      if (!base::CreateDirectory(path))
        return base::FilePath();

      // Create a zero-byte ".localized" file to inherit localizations from OSX
      // for folders that have special meaning.
      base::WriteFile(path.Append(".localized"), NULL, 0);
    }
    return base::PathIsWritable(path) ? path : base::FilePath();
  }
  return base::FilePath();
}

// Given the path to an app bundle, return the resources directory.
base::FilePath GetResourcesPath(const base::FilePath& app_path) {
  return app_path.Append("Contents").Append("Resources");
}

bool HasExistingExtensionShim(const base::FilePath& destination_directory,
                              const std::string& extension_id,
                              const base::FilePath& own_basename) {
  // Check if there any any other shims for the same extension.
  base::FileEnumerator enumerator(destination_directory,
                                  false /* recursive */,
                                  base::FileEnumerator::DIRECTORIES);
  for (base::FilePath shim_path = enumerator.Next();
       !shim_path.empty(); shim_path = enumerator.Next()) {
    if (shim_path.BaseName() != own_basename &&
        EndsWith(shim_path.RemoveExtension().value(),
                 extension_id,
                 true /* case_sensitive */)) {
      return true;
    }
  }

  return false;
}

// Given the path to an app bundle, return the path to the Info.plist file.
NSString* GetPlistPath(const base::FilePath& bundle_path) {
  return base::mac::FilePathToNSString(
      bundle_path.Append("Contents").Append("Info.plist"));
}

NSMutableDictionary* ReadPlist(NSString* plist_path) {
  return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
}

// Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
// the Info.plist starts with the current user_data_dir. This uses starts with
// instead of equals because the CrAppModeUserDataDir could be the user_data_dir
// or the |app_data_dir_|.
bool HasSameUserDataDir(const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
  base::FilePath user_data_dir;
  PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
  DCHECK(!user_data_dir.empty());
  return StartsWithASCII(
      base::SysNSStringToUTF8(
          [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
      user_data_dir.value(),
      true /* case_sensitive */);
}

void LaunchShimOnFileThread(scoped_ptr<web_app::ShortcutInfo> shortcut_info,
                            bool launched_after_rebuild) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
  base::FilePath shim_path = web_app::GetAppInstallPath(*shortcut_info);

  if (shim_path.empty() ||
      !base::PathExists(shim_path) ||
      !HasSameUserDataDir(shim_path)) {
    // The user may have deleted the copy in the Applications folder, use the
    // one in the web app's |app_data_dir_|.
    base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
        shortcut_info->profile_path, shortcut_info->extension_id, GURL());
    shim_path = app_data_dir.Append(shim_path.BaseName());
  }

  if (!base::PathExists(shim_path))
    return;

  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII(
      app_mode::kLaunchedByChromeProcessId,
      base::IntToString(base::GetCurrentProcId()));
  if (launched_after_rebuild)
    command_line.AppendSwitch(app_mode::kLaunchedAfterRebuild);
  // Launch without activating (kLSLaunchDontSwitch).
  base::mac::OpenApplicationWithPath(
      shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
}

base::FilePath GetAppLoaderPath() {
  return base::mac::PathForFrameworkBundleResource(
      base::mac::NSToCFCast(@"app_mode_loader.app"));
}

void UpdatePlatformShortcutsInternal(
    const base::FilePath& app_data_path,
    const base::string16& old_app_title,
    const web_app::ShortcutInfo& shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
  if (AppShimsDisabledForTest() &&
      !g_app_shims_allow_update_and_launch_in_tests) {
    return;
  }

  web_app::WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info,
                                                  file_handlers_info);
  shortcut_creator.UpdateShortcuts();
}

void UpdateAndLaunchShimOnFileThread(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  base::FilePath shortcut_data_dir = web_app::GetWebAppDataDirectory(
      shortcut_info->profile_path, shortcut_info->extension_id, GURL());
  UpdatePlatformShortcutsInternal(shortcut_data_dir, base::string16(),
                                  *shortcut_info, file_handlers_info);
  LaunchShimOnFileThread(shortcut_info.Pass(), true);
}

void UpdateAndLaunchShim(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  content::BrowserThread::PostTask(
      content::BrowserThread::FILE, FROM_HERE,
      base::Bind(&UpdateAndLaunchShimOnFileThread, base::Passed(&shortcut_info),
                 file_handlers_info));
}

void RebuildAppAndLaunch(scoped_ptr<web_app::ShortcutInfo> shortcut_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (shortcut_info->extension_id == app_mode::kAppListModeId) {
    AppListService* app_list_service =
        AppListService::Get(chrome::HOST_DESKTOP_TYPE_NATIVE);
    app_list_service->CreateShortcut();
    app_list_service->Show();
    return;
  }

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  Profile* profile =
      profile_manager->GetProfileByPath(shortcut_info->profile_path);
  if (!profile || !profile_manager->IsValidProfile(profile))
    return;

  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile);
  const extensions::Extension* extension = registry->GetExtensionById(
      shortcut_info->extension_id, extensions::ExtensionRegistry::ENABLED);
  if (!extension || !extension->is_platform_app())
    return;

  web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim));
}

base::FilePath GetLocalizableAppShortcutsSubdirName() {
  static const char kChromiumAppDirName[] = "Chromium Apps.localized";
  static const char kChromeAppDirName[] = "Chrome Apps.localized";
  static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";

  switch (chrome::VersionInfo::GetChannel()) {
    case chrome::VersionInfo::CHANNEL_UNKNOWN:
      return base::FilePath(kChromiumAppDirName);

    case chrome::VersionInfo::CHANNEL_CANARY:
      return base::FilePath(kChromeCanaryAppDirName);

    default:
      return base::FilePath(kChromeAppDirName);
  }
}

// Creates a canvas the same size as |overlay|, copies the appropriate
// representation from |backgound| into it (according to Cocoa), then draws
// |overlay| over it using NSCompositeSourceOver.
NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
  DCHECK(background);
  NSInteger dimension = [overlay pixelsWide];
  DCHECK_EQ(dimension, [overlay pixelsHigh]);
  base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes:NULL
                    pixelsWide:dimension
                    pixelsHigh:dimension
                 bitsPerSample:8
               samplesPerPixel:4
                      hasAlpha:YES
                      isPlanar:NO
                colorSpaceName:NSCalibratedRGBColorSpace
                   bytesPerRow:0
                  bitsPerPixel:0]);

  // There isn't a colorspace name constant for sRGB, so retag.
  NSBitmapImageRep* srgb_canvas = [canvas
      bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
  canvas.reset([srgb_canvas retain]);

  // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
  [canvas setSize:NSMakeSize(dimension, dimension)];

  NSGraphicsContext* drawing_context =
      [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
  [NSGraphicsContext saveGraphicsState];
  [NSGraphicsContext setCurrentContext:drawing_context];
  [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
                fromRect:NSZeroRect
               operation:NSCompositeCopy
                fraction:1.0];
  [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
             fromRect:NSZeroRect
            operation:NSCompositeSourceOver
             fraction:1.0
       respectFlipped:NO
                hints:0];
  [NSGraphicsContext restoreGraphicsState];
  return canvas.autorelease();
}

// Helper function to extract the single NSImageRep held in a resource bundle
// image.
NSImageRep* ImageRepForResource(int resource_id) {
  gfx::Image& image =
      ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
  NSArray* image_reps = [image.AsNSImage() representations];
  DCHECK_EQ(1u, [image_reps count]);
  return [image_reps objectAtIndex:0];
}

// Adds a localized strings file for the Chrome Apps directory using the current
// locale. OSX will use this for the display name.
// + Chrome Apps.localized (|apps_directory|)
// | + .localized
// | | en.strings
// | | de.strings
void UpdateAppShortcutsSubdirLocalizedName(
    const base::FilePath& apps_directory) {
  base::FilePath localized = apps_directory.Append(".localized");
  if (!base::CreateDirectory(localized))
    return;

  base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
  base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
  NSDictionary* strings_dict = @{
      base::mac::FilePathToNSString(directory_name) :
          base::SysUTF16ToNSString(localized_name)
  };

  std::string locale = l10n_util::NormalizeLocale(
      l10n_util::GetApplicationLocale(std::string()));

  NSString* strings_path = base::mac::FilePathToNSString(
      localized.Append(locale + ".strings"));
  [strings_dict writeToFile:strings_path
                 atomically:YES];

  base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);

  // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a
  // bug when dealing with named NSImages where it incorrectly handles alpha
  // premultiplication. This is most noticable with small assets since the 1px
  // border is a much larger component of the small icons.
  // See http://crbug.com/305373 for details.
  [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)];
  [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)];

  // Brand larger folder assets with an embossed app launcher logo to conserve
  // distro size and for better consistency with changing hue across OSX
  // versions. The folder is textured, so compresses poorly without this.
  const int kBrandResourceIds[] = {
    IDR_APPS_FOLDER_OVERLAY_128,
    IDR_APPS_FOLDER_OVERLAY_512,
  };
  NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
  for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
    NSImageRep* with_overlay =
        OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i]));
    DCHECK(with_overlay);
    if (with_overlay)
      [folder_icon_image addRepresentation:with_overlay];
  }
  [[NSWorkspace sharedWorkspace]
      setIcon:folder_icon_image
      forFile:base::mac::FilePathToNSString(apps_directory)
      options:0];
}

void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
  DCHECK(!app_path.empty());
  base::DeleteFile(app_path, true);
  base::FilePath apps_folder = app_path.DirName();
  if (base::IsDirectoryEmpty(apps_folder))
    base::DeleteFile(apps_folder, false);
}

bool IsShimForProfile(const base::FilePath& base_name,
                      const std::string& profile_base_name) {
  if (!StartsWithASCII(base_name.value(), profile_base_name, true))
    return false;

  if (base_name.Extension() != ".app")
    return false;

  std::string app_id = base_name.RemoveExtension().value();
  // Strip (profile_base_name + " ") from the start.
  app_id = app_id.substr(profile_base_name.size() + 1);
  return crx_file::id_util::IdIsValid(app_id);
}

std::vector<base::FilePath> GetAllAppBundlesInPath(
    const base::FilePath& internal_shortcut_path,
    const std::string& profile_base_name) {
  std::vector<base::FilePath> bundle_paths;

  base::FileEnumerator enumerator(internal_shortcut_path,
                                  true /* recursive */,
                                  base::FileEnumerator::DIRECTORIES);
  for (base::FilePath bundle_path = enumerator.Next();
       !bundle_path.empty(); bundle_path = enumerator.Next()) {
    if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
      bundle_paths.push_back(bundle_path);
  }

  return bundle_paths;
}

scoped_ptr<web_app::ShortcutInfo> BuildShortcutInfoFromBundle(
    const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));

  scoped_ptr<web_app::ShortcutInfo> shortcut_info(new web_app::ShortcutInfo);
  shortcut_info->extension_id = base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
  shortcut_info->is_platform_app = true;
  shortcut_info->url = GURL(base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
  shortcut_info->title = base::SysNSStringToUTF16(
      [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
  shortcut_info->profile_name = base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);

  // Figure out the profile_path. Since the user_data_dir could contain the
  // path to the web app data dir.
  base::FilePath user_data_dir = base::mac::NSStringToFilePath(
      [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
  base::FilePath profile_base_name = base::mac::NSStringToFilePath(
      [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
  if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
    shortcut_info->profile_path = user_data_dir.DirName().DirName();
  else
    shortcut_info->profile_path = user_data_dir.Append(profile_base_name);

  return shortcut_info;
}

scoped_ptr<web_app::ShortcutInfo> RecordAppShimErrorAndBuildShortcutInfo(
    const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
  NSString* version_string = [plist valueForKey:app_mode::kCrBundleVersionKey];
  if (!version_string) {
    // Older bundles have the Chrome version in the following key.
    version_string =
        [plist valueForKey:app_mode::kCFBundleShortVersionStringKey];
  }
  base::Version full_version(base::SysNSStringToUTF8(version_string));
  uint32_t major_version = 0;
  if (full_version.IsValid())
    major_version = full_version.components()[0];
  UMA_HISTOGRAM_SPARSE_SLOWLY("Apps.AppShimErrorVersion", major_version);

  return BuildShortcutInfoFromBundle(bundle_path);
}

void UpdateFileTypes(NSMutableDictionary* plist,
                     const extensions::FileHandlersInfo& file_handlers_info) {
  NSMutableArray* document_types =
      [NSMutableArray arrayWithCapacity:file_handlers_info.size()];

  for (extensions::FileHandlersInfo::const_iterator info_it =
           file_handlers_info.begin();
       info_it != file_handlers_info.end();
       ++info_it) {
    const extensions::FileHandlerInfo& info = *info_it;

    NSMutableArray* file_extensions =
        [NSMutableArray arrayWithCapacity:info.extensions.size()];
    for (std::set<std::string>::iterator it = info.extensions.begin();
         it != info.extensions.end();
         ++it) {
      [file_extensions addObject:base::SysUTF8ToNSString(*it)];
    }

    NSMutableArray* mime_types =
        [NSMutableArray arrayWithCapacity:info.types.size()];
    for (std::set<std::string>::iterator it = info.types.begin();
         it != info.types.end();
         ++it) {
      [mime_types addObject:base::SysUTF8ToNSString(*it)];
    }

    NSDictionary* type_dictionary = @{
      // TODO(jackhou): Add the type name and and icon file once the manifest
      // supports these.
      // app_mode::kCFBundleTypeNameKey : ,
      // app_mode::kCFBundleTypeIconFileKey : ,
      app_mode::kCFBundleTypeExtensionsKey : file_extensions,
      app_mode::kCFBundleTypeMIMETypesKey : mime_types,
      app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer
    };
    [document_types addObject:type_dictionary];
  }

  [plist setObject:document_types
            forKey:app_mode::kCFBundleDocumentTypesKey];
}

void RevealAppShimInFinderForAppOnFileThread(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const base::FilePath& app_path) {
  web_app::WebAppShortcutCreator shortcut_creator(
      app_path, shortcut_info.get(), extensions::FileHandlersInfo());
  shortcut_creator.RevealAppShimInFinder();
}

}  // namespace

@interface CrCreateAppShortcutCheckboxObserver : NSObject {
 @private
  NSButton* checkbox_;
  NSButton* continueButton_;
}

- (id)initWithCheckbox:(NSButton*)checkbox
        continueButton:(NSButton*)continueButton;
- (void)startObserving;
- (void)stopObserving;
@end

@implementation CrCreateAppShortcutCheckboxObserver

- (id)initWithCheckbox:(NSButton*)checkbox
        continueButton:(NSButton*)continueButton {
  if ((self = [super init])) {
    checkbox_ = checkbox;
    continueButton_ = continueButton;
  }
  return self;
}

- (void)startObserving {
  [checkbox_ addObserver:self
              forKeyPath:@"cell.state"
                 options:0
                 context:nil];
}

- (void)stopObserving {
  [checkbox_ removeObserver:self
                 forKeyPath:@"cell.state"];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  [continueButton_ setEnabled:([checkbox_ state] == NSOnState)];
}

@end

namespace web_app {

WebAppShortcutCreator::WebAppShortcutCreator(
    const base::FilePath& app_data_dir,
    const ShortcutInfo* shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info)
    : app_data_dir_(app_data_dir),
      info_(shortcut_info),
      file_handlers_info_(file_handlers_info) {
  DCHECK(shortcut_info);
}

WebAppShortcutCreator::~WebAppShortcutCreator() {}

base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
  base::FilePath applications_dir = GetApplicationsDirname();
  return applications_dir.empty() ?
      base::FilePath() : applications_dir.Append(GetShortcutBasename());
}

base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
  return app_data_dir_.Append(GetShortcutBasename());
}

base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
  std::string app_name;
  // Check if there should be a separate shortcut made for different profiles.
  // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
  // otherwise it will be empty.
  if (!info_->profile_name.empty()) {
    app_name += info_->profile_path.BaseName().value();
    app_name += ' ';
  }
  app_name += info_->extension_id;
  return base::FilePath(app_name).ReplaceExtension("app");
}

bool WebAppShortcutCreator::BuildShortcut(
    const base::FilePath& staging_path) const {
  // Update the app's plist and icon in a temp directory. This works around
  // a Finder bug where the app's icon doesn't properly update.
  if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
    LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
               << " failed.";
    return false;
  }

  return UpdatePlist(staging_path) &&
      UpdateDisplayName(staging_path) &&
      UpdateIcon(staging_path);
}

size_t WebAppShortcutCreator::CreateShortcutsIn(
    const std::vector<base::FilePath>& folders) const {
  size_t succeeded = 0;

  base::ScopedTempDir scoped_temp_dir;
  if (!scoped_temp_dir.CreateUniqueTempDir())
    return 0;

  base::FilePath app_name = GetShortcutBasename();
  base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
  if (!BuildShortcut(staging_path))
    return 0;

  for (std::vector<base::FilePath>::const_iterator it = folders.begin();
       it != folders.end(); ++it) {
    const base::FilePath& dst_path = *it;
    if (!base::CreateDirectory(dst_path)) {
      LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
      return succeeded;
    }

    if (!base::CopyDirectory(staging_path, dst_path, true)) {
      LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
                 << " failed";
      return succeeded;
    }

    // Remove the quarantine attribute from both the bundle and the executable.
    base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
    base::mac::RemoveQuarantineAttribute(
        dst_path.Append(app_name)
            .Append("Contents").Append("MacOS").Append("app_mode_loader"));
    ++succeeded;
  }

  return succeeded;
}

bool WebAppShortcutCreator::CreateShortcuts(
    ShortcutCreationReason creation_reason,
    ShortcutLocations creation_locations) {
  const base::FilePath applications_dir = GetApplicationsDirname();
  if (applications_dir.empty() ||
      !base::DirectoryExists(applications_dir.DirName())) {
    LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
    return false;
  }

  UpdateAppShortcutsSubdirLocalizedName(applications_dir);

  // If non-nil, this path is added to the OSX Dock after creating shortcuts.
  NSString* path_to_add_to_dock = nil;

  std::vector<base::FilePath> paths;

  // The app list shim is not tied to a particular profile, so omit the copy
  // placed under the profile path. For shims, this copy is used when the
  // version under Applications is removed, and not needed for app list because
  // setting LSUIElement means there is no Dock "running" status to show.
  const bool is_app_list = info_->extension_id == app_mode::kAppListModeId;
  if (is_app_list) {
    path_to_add_to_dock = base::SysUTF8ToNSString(
        applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
  } else {
    paths.push_back(app_data_dir_);
  }

  bool shortcut_visible =
      creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN;
  if (shortcut_visible)
    paths.push_back(applications_dir);

  DCHECK(!paths.empty());
  size_t success_count = CreateShortcutsIn(paths);
  if (success_count == 0)
    return false;

  if (!is_app_list)
    UpdateInternalBundleIdentifier();

  if (success_count != paths.size())
    return false;

  if (creation_locations.in_quick_launch_bar && path_to_add_to_dock &&
      shortcut_visible) {
    switch (dock::AddIcon(path_to_add_to_dock, nil)) {
      case dock::IconAddFailure:
        // If adding the icon failed, instead reveal the Finder window.
        RevealAppShimInFinder();
        break;
      case dock::IconAddSuccess:
      case dock::IconAlreadyPresent:
        break;
    }
    return true;
  }

  if (creation_reason == SHORTCUT_CREATION_BY_USER)
    RevealAppShimInFinder();

  return true;
}

void WebAppShortcutCreator::DeleteShortcuts() {
  base::FilePath app_path = GetApplicationsShortcutPath();
  if (!app_path.empty() && HasSameUserDataDir(app_path))
    DeletePathAndParentIfEmpty(app_path);

  // In case the user has moved/renamed/copied the app bundle.
  base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
  if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
    base::DeleteFile(bundle_path, true);

  // Delete the internal one.
  DeletePathAndParentIfEmpty(GetInternalShortcutPath());
}

bool WebAppShortcutCreator::UpdateShortcuts() {
  std::vector<base::FilePath> paths;
  base::DeleteFile(GetInternalShortcutPath(), true);
  paths.push_back(app_data_dir_);

  // Try to update the copy under /Applications. If that does not exist, check
  // if a matching bundle can be found elsewhere.
  base::FilePath app_path = GetApplicationsShortcutPath();
  if (app_path.empty() || !base::PathExists(app_path))
    app_path = GetAppBundleById(GetBundleIdentifier());

  if (!app_path.empty()) {
    base::DeleteFile(app_path, true);
    paths.push_back(app_path.DirName());
  }

  size_t success_count = CreateShortcutsIn(paths);
  if (success_count == 0)
    return false;

  UpdateInternalBundleIdentifier();
  return success_count == paths.size() && !app_path.empty();
}

base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
  base::FilePath path = GetWritableApplicationsDirectory();
  if (path.empty())
    return path;

  return path.Append(GetLocalizableAppShortcutsSubdirName());
}

bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
  NSString* extension_id = base::SysUTF8ToNSString(info_->extension_id);
  NSString* extension_title = base::SysUTF16ToNSString(info_->title);
  NSString* extension_url = base::SysUTF8ToNSString(info_->url.spec());
  NSString* chrome_bundle_id =
      base::SysUTF8ToNSString(base::mac::BaseBundleID());
  NSDictionary* replacement_dict =
      [NSDictionary dictionaryWithObjectsAndKeys:
          extension_id, app_mode::kShortcutIdPlaceholder,
          extension_title, app_mode::kShortcutNamePlaceholder,
          extension_url, app_mode::kShortcutURLPlaceholder,
          chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
          nil];

  NSString* plist_path = GetPlistPath(app_path);
  NSMutableDictionary* plist = ReadPlist(plist_path);
  NSArray* keys = [plist allKeys];

  // 1. Fill in variables.
  for (id key in keys) {
    NSString* value = [plist valueForKey:key];
    if (![value isKindOfClass:[NSString class]] || [value length] < 2)
      continue;

    // Remove leading and trailing '@'s.
    NSString* variable =
        [value substringWithRange:NSMakeRange(1, [value length] - 2)];

    NSString* substitution = [replacement_dict valueForKey:variable];
    if (substitution)
      [plist setObject:substitution forKey:key];
  }

  // 2. Fill in other values.
  [plist setObject:base::SysUTF8ToNSString(chrome::VersionInfo().Version())
            forKey:app_mode::kCrBundleVersionKey];
  [plist setObject:base::SysUTF8ToNSString(info_->version_for_display)
            forKey:app_mode::kCFBundleShortVersionStringKey];
  [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
            forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
  [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
            forKey:app_mode::kCrAppModeUserDataDirKey];
  [plist setObject:base::mac::FilePathToNSString(info_->profile_path.BaseName())
            forKey:app_mode::kCrAppModeProfileDirKey];
  [plist setObject:base::SysUTF8ToNSString(info_->profile_name)
            forKey:app_mode::kCrAppModeProfileNameKey];
  [plist setObject:[NSNumber numberWithBool:YES]
            forKey:app_mode::kLSHasLocalizedDisplayNameKey];
  if (info_->extension_id == app_mode::kAppListModeId) {
    // Prevent the app list from bouncing in the dock, and getting a run light.
    [plist setObject:[NSNumber numberWithBool:YES]
              forKey:kLSUIElement];
  }

  base::FilePath app_name = app_path.BaseName().RemoveExtension();
  [plist setObject:base::mac::FilePathToNSString(app_name)
            forKey:base::mac::CFToNSCast(kCFBundleNameKey)];

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kEnableAppsFileAssociations)) {
    UpdateFileTypes(plist, file_handlers_info_);
  }

  return [plist writeToFile:plist_path
                 atomically:YES];
}

bool WebAppShortcutCreator::UpdateDisplayName(
    const base::FilePath& app_path) const {
  // OSX searches for the best language in the order of preferred languages.
  // Since we only have one localization directory, it will choose this one.
  base::FilePath localized_dir = GetResourcesPath(app_path).Append("en.lproj");
  if (!base::CreateDirectory(localized_dir))
    return false;

  NSString* bundle_name = base::SysUTF16ToNSString(info_->title);
  NSString* display_name = base::SysUTF16ToNSString(info_->title);
  if (HasExistingExtensionShim(GetApplicationsDirname(), info_->extension_id,
                               app_path.BaseName())) {
    display_name = [bundle_name
        stringByAppendingString:base::SysUTF8ToNSString(
            " (" + info_->profile_name + ")")];
  }

  NSDictionary* strings_plist = @{
    base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
    app_mode::kCFBundleDisplayNameKey : display_name
  };<commit_msg>[Mac] Localize app shims with the OS preferred language.<commit_after>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/web_applications/web_app_mac.h"

#import <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>

#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/sparse_histogram.h"
#include "base/path_service.h"
#include "base/process/process_handle.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#import "chrome/browser/mac/dock.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/ui/app_list/app_list_service.h"
#include "chrome/browser/ui/cocoa/key_equivalent_constants.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/chrome_version_info.h"
#import "chrome/common/mac/app_mode_common.h"
#include "chrome/grit/generated_resources.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "grit/chrome_unscaled_resources.h"
#import "skia/ext/skia_utils_mac.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image_family.h"

bool g_app_shims_allow_update_and_launch_in_tests = false;

namespace {

// Launch Services Key to run as an agent app, which doesn't launch in the dock.
NSString* const kLSUIElement = @"LSUIElement";

class ScopedCarbonHandle {
 public:
  ScopedCarbonHandle(size_t initial_size) : handle_(NewHandle(initial_size)) {
    DCHECK(handle_);
    DCHECK_EQ(noErr, MemError());
  }
  ~ScopedCarbonHandle() { DisposeHandle(handle_); }

  Handle Get() { return handle_; }
  char* Data() { return *handle_; }
  size_t HandleSize() const { return GetHandleSize(handle_); }

  IconFamilyHandle GetAsIconFamilyHandle() {
    return reinterpret_cast<IconFamilyHandle>(handle_);
  }

  bool WriteDataToFile(const base::FilePath& path) {
    NSData* data = [NSData dataWithBytes:Data()
                                  length:HandleSize()];
    return [data writeToFile:base::mac::FilePathToNSString(path)
                  atomically:NO];
  }

 private:
  Handle handle_;
};

void ConvertSkiaToARGB(const SkBitmap& bitmap, ScopedCarbonHandle* handle) {
  CHECK_EQ(4u * bitmap.width() * bitmap.height(), handle->HandleSize());

  char* argb = handle->Data();
  SkAutoLockPixels lock(bitmap);
  for (int y = 0; y < bitmap.height(); ++y) {
    for (int x = 0; x < bitmap.width(); ++x) {
      SkColor pixel = bitmap.getColor(x, y);
      argb[0] = SkColorGetA(pixel);
      argb[1] = SkColorGetR(pixel);
      argb[2] = SkColorGetG(pixel);
      argb[3] = SkColorGetB(pixel);
      argb += 4;
    }
  }
}

// Adds |image| to |icon_family|. Returns true on success, false on failure.
bool AddGfxImageToIconFamily(IconFamilyHandle icon_family,
                             const gfx::Image& image) {
  // When called via ShowCreateChromeAppShortcutsDialog the ImageFamily will
  // have all the representations desired here for mac, from the kDesiredSizes
  // array in web_app.cc.
  SkBitmap bitmap = image.AsBitmap();
  if (bitmap.colorType() != kN32_SkColorType ||
      bitmap.width() != bitmap.height()) {
    return false;
  }

  OSType icon_type;
  switch (bitmap.width()) {
    case 512:
      icon_type = kIconServices512PixelDataARGB;
      break;
    case 256:
      icon_type = kIconServices256PixelDataARGB;
      break;
    case 128:
      icon_type = kIconServices128PixelDataARGB;
      break;
    case 48:
      icon_type = kIconServices48PixelDataARGB;
      break;
    case 32:
      icon_type = kIconServices32PixelDataARGB;
      break;
    case 16:
      icon_type = kIconServices16PixelDataARGB;
      break;
    default:
      return false;
  }

  ScopedCarbonHandle raw_data(bitmap.getSize());
  ConvertSkiaToARGB(bitmap, &raw_data);
  OSErr result = SetIconFamilyData(icon_family, icon_type, raw_data.Get());
  DCHECK_EQ(noErr, result);
  return result == noErr;
}

bool AppShimsDisabledForTest() {
  // Disable app shims in tests because shims created in ~/Applications will not
  // be cleaned up.
  return base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType);
}

base::FilePath GetWritableApplicationsDirectory() {
  base::FilePath path;
  if (base::mac::GetUserDirectory(NSApplicationDirectory, &path)) {
    if (!base::DirectoryExists(path)) {
      if (!base::CreateDirectory(path))
        return base::FilePath();

      // Create a zero-byte ".localized" file to inherit localizations from OSX
      // for folders that have special meaning.
      base::WriteFile(path.Append(".localized"), NULL, 0);
    }
    return base::PathIsWritable(path) ? path : base::FilePath();
  }
  return base::FilePath();
}

// Given the path to an app bundle, return the resources directory.
base::FilePath GetResourcesPath(const base::FilePath& app_path) {
  return app_path.Append("Contents").Append("Resources");
}

bool HasExistingExtensionShim(const base::FilePath& destination_directory,
                              const std::string& extension_id,
                              const base::FilePath& own_basename) {
  // Check if there any any other shims for the same extension.
  base::FileEnumerator enumerator(destination_directory,
                                  false /* recursive */,
                                  base::FileEnumerator::DIRECTORIES);
  for (base::FilePath shim_path = enumerator.Next();
       !shim_path.empty(); shim_path = enumerator.Next()) {
    if (shim_path.BaseName() != own_basename &&
        EndsWith(shim_path.RemoveExtension().value(),
                 extension_id,
                 true /* case_sensitive */)) {
      return true;
    }
  }

  return false;
}

// Given the path to an app bundle, return the path to the Info.plist file.
NSString* GetPlistPath(const base::FilePath& bundle_path) {
  return base::mac::FilePathToNSString(
      bundle_path.Append("Contents").Append("Info.plist"));
}

NSMutableDictionary* ReadPlist(NSString* plist_path) {
  return [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
}

// Takes the path to an app bundle and checks that the CrAppModeUserDataDir in
// the Info.plist starts with the current user_data_dir. This uses starts with
// instead of equals because the CrAppModeUserDataDir could be the user_data_dir
// or the |app_data_dir_|.
bool HasSameUserDataDir(const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
  base::FilePath user_data_dir;
  PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
  DCHECK(!user_data_dir.empty());
  return StartsWithASCII(
      base::SysNSStringToUTF8(
          [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]),
      user_data_dir.value(),
      true /* case_sensitive */);
}

void LaunchShimOnFileThread(scoped_ptr<web_app::ShortcutInfo> shortcut_info,
                            bool launched_after_rebuild) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
  base::FilePath shim_path = web_app::GetAppInstallPath(*shortcut_info);

  if (shim_path.empty() ||
      !base::PathExists(shim_path) ||
      !HasSameUserDataDir(shim_path)) {
    // The user may have deleted the copy in the Applications folder, use the
    // one in the web app's |app_data_dir_|.
    base::FilePath app_data_dir = web_app::GetWebAppDataDirectory(
        shortcut_info->profile_path, shortcut_info->extension_id, GURL());
    shim_path = app_data_dir.Append(shim_path.BaseName());
  }

  if (!base::PathExists(shim_path))
    return;

  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  command_line.AppendSwitchASCII(
      app_mode::kLaunchedByChromeProcessId,
      base::IntToString(base::GetCurrentProcId()));
  if (launched_after_rebuild)
    command_line.AppendSwitch(app_mode::kLaunchedAfterRebuild);
  // Launch without activating (kLSLaunchDontSwitch).
  base::mac::OpenApplicationWithPath(
      shim_path, command_line, kLSLaunchDefaults | kLSLaunchDontSwitch, NULL);
}

base::FilePath GetAppLoaderPath() {
  return base::mac::PathForFrameworkBundleResource(
      base::mac::NSToCFCast(@"app_mode_loader.app"));
}

void UpdatePlatformShortcutsInternal(
    const base::FilePath& app_data_path,
    const base::string16& old_app_title,
    const web_app::ShortcutInfo& shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
  if (AppShimsDisabledForTest() &&
      !g_app_shims_allow_update_and_launch_in_tests) {
    return;
  }

  web_app::WebAppShortcutCreator shortcut_creator(app_data_path, &shortcut_info,
                                                  file_handlers_info);
  shortcut_creator.UpdateShortcuts();
}

void UpdateAndLaunchShimOnFileThread(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  base::FilePath shortcut_data_dir = web_app::GetWebAppDataDirectory(
      shortcut_info->profile_path, shortcut_info->extension_id, GURL());
  UpdatePlatformShortcutsInternal(shortcut_data_dir, base::string16(),
                                  *shortcut_info, file_handlers_info);
  LaunchShimOnFileThread(shortcut_info.Pass(), true);
}

void UpdateAndLaunchShim(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info) {
  content::BrowserThread::PostTask(
      content::BrowserThread::FILE, FROM_HERE,
      base::Bind(&UpdateAndLaunchShimOnFileThread, base::Passed(&shortcut_info),
                 file_handlers_info));
}

void RebuildAppAndLaunch(scoped_ptr<web_app::ShortcutInfo> shortcut_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (shortcut_info->extension_id == app_mode::kAppListModeId) {
    AppListService* app_list_service =
        AppListService::Get(chrome::HOST_DESKTOP_TYPE_NATIVE);
    app_list_service->CreateShortcut();
    app_list_service->Show();
    return;
  }

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  Profile* profile =
      profile_manager->GetProfileByPath(shortcut_info->profile_path);
  if (!profile || !profile_manager->IsValidProfile(profile))
    return;

  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile);
  const extensions::Extension* extension = registry->GetExtensionById(
      shortcut_info->extension_id, extensions::ExtensionRegistry::ENABLED);
  if (!extension || !extension->is_platform_app())
    return;

  web_app::GetInfoForApp(extension, profile, base::Bind(&UpdateAndLaunchShim));
}

base::FilePath GetLocalizableAppShortcutsSubdirName() {
  static const char kChromiumAppDirName[] = "Chromium Apps.localized";
  static const char kChromeAppDirName[] = "Chrome Apps.localized";
  static const char kChromeCanaryAppDirName[] = "Chrome Canary Apps.localized";

  switch (chrome::VersionInfo::GetChannel()) {
    case chrome::VersionInfo::CHANNEL_UNKNOWN:
      return base::FilePath(kChromiumAppDirName);

    case chrome::VersionInfo::CHANNEL_CANARY:
      return base::FilePath(kChromeCanaryAppDirName);

    default:
      return base::FilePath(kChromeAppDirName);
  }
}

// Creates a canvas the same size as |overlay|, copies the appropriate
// representation from |backgound| into it (according to Cocoa), then draws
// |overlay| over it using NSCompositeSourceOver.
NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
  DCHECK(background);
  NSInteger dimension = [overlay pixelsWide];
  DCHECK_EQ(dimension, [overlay pixelsHigh]);
  base::scoped_nsobject<NSBitmapImageRep> canvas([[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes:NULL
                    pixelsWide:dimension
                    pixelsHigh:dimension
                 bitsPerSample:8
               samplesPerPixel:4
                      hasAlpha:YES
                      isPlanar:NO
                colorSpaceName:NSCalibratedRGBColorSpace
                   bytesPerRow:0
                  bitsPerPixel:0]);

  // There isn't a colorspace name constant for sRGB, so retag.
  NSBitmapImageRep* srgb_canvas = [canvas
      bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]];
  canvas.reset([srgb_canvas retain]);

  // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
  [canvas setSize:NSMakeSize(dimension, dimension)];

  NSGraphicsContext* drawing_context =
      [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
  [NSGraphicsContext saveGraphicsState];
  [NSGraphicsContext setCurrentContext:drawing_context];
  [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
                fromRect:NSZeroRect
               operation:NSCompositeCopy
                fraction:1.0];
  [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
             fromRect:NSZeroRect
            operation:NSCompositeSourceOver
             fraction:1.0
       respectFlipped:NO
                hints:0];
  [NSGraphicsContext restoreGraphicsState];
  return canvas.autorelease();
}

// Helper function to extract the single NSImageRep held in a resource bundle
// image.
NSImageRep* ImageRepForResource(int resource_id) {
  gfx::Image& image =
      ResourceBundle::GetSharedInstance().GetNativeImageNamed(resource_id);
  NSArray* image_reps = [image.AsNSImage() representations];
  DCHECK_EQ(1u, [image_reps count]);
  return [image_reps objectAtIndex:0];
}

// Adds a localized strings file for the Chrome Apps directory using the current
// locale. OSX will use this for the display name.
// + Chrome Apps.localized (|apps_directory|)
// | + .localized
// | | en.strings
// | | de.strings
void UpdateAppShortcutsSubdirLocalizedName(
    const base::FilePath& apps_directory) {
  base::FilePath localized = apps_directory.Append(".localized");
  if (!base::CreateDirectory(localized))
    return;

  base::FilePath directory_name = apps_directory.BaseName().RemoveExtension();
  base::string16 localized_name = ShellIntegration::GetAppShortcutsSubdirName();
  NSDictionary* strings_dict = @{
      base::mac::FilePathToNSString(directory_name) :
          base::SysUTF16ToNSString(localized_name)
  };

  std::string locale = l10n_util::NormalizeLocale(
      l10n_util::GetApplicationLocale(std::string()));

  NSString* strings_path = base::mac::FilePathToNSString(
      localized.Append(locale + ".strings"));
  [strings_dict writeToFile:strings_path
                 atomically:YES];

  base::scoped_nsobject<NSImage> folder_icon_image([[NSImage alloc] init]);

  // Use complete assets for the small icon sizes. -[NSWorkspace setIcon:] has a
  // bug when dealing with named NSImages where it incorrectly handles alpha
  // premultiplication. This is most noticable with small assets since the 1px
  // border is a much larger component of the small icons.
  // See http://crbug.com/305373 for details.
  [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_16)];
  [folder_icon_image addRepresentation:ImageRepForResource(IDR_APPS_FOLDER_32)];

  // Brand larger folder assets with an embossed app launcher logo to conserve
  // distro size and for better consistency with changing hue across OSX
  // versions. The folder is textured, so compresses poorly without this.
  const int kBrandResourceIds[] = {
    IDR_APPS_FOLDER_OVERLAY_128,
    IDR_APPS_FOLDER_OVERLAY_512,
  };
  NSImage* base_image = [NSImage imageNamed:NSImageNameFolder];
  for (size_t i = 0; i < arraysize(kBrandResourceIds); ++i) {
    NSImageRep* with_overlay =
        OverlayImageRep(base_image, ImageRepForResource(kBrandResourceIds[i]));
    DCHECK(with_overlay);
    if (with_overlay)
      [folder_icon_image addRepresentation:with_overlay];
  }
  [[NSWorkspace sharedWorkspace]
      setIcon:folder_icon_image
      forFile:base::mac::FilePathToNSString(apps_directory)
      options:0];
}

void DeletePathAndParentIfEmpty(const base::FilePath& app_path) {
  DCHECK(!app_path.empty());
  base::DeleteFile(app_path, true);
  base::FilePath apps_folder = app_path.DirName();
  if (base::IsDirectoryEmpty(apps_folder))
    base::DeleteFile(apps_folder, false);
}

bool IsShimForProfile(const base::FilePath& base_name,
                      const std::string& profile_base_name) {
  if (!StartsWithASCII(base_name.value(), profile_base_name, true))
    return false;

  if (base_name.Extension() != ".app")
    return false;

  std::string app_id = base_name.RemoveExtension().value();
  // Strip (profile_base_name + " ") from the start.
  app_id = app_id.substr(profile_base_name.size() + 1);
  return crx_file::id_util::IdIsValid(app_id);
}

std::vector<base::FilePath> GetAllAppBundlesInPath(
    const base::FilePath& internal_shortcut_path,
    const std::string& profile_base_name) {
  std::vector<base::FilePath> bundle_paths;

  base::FileEnumerator enumerator(internal_shortcut_path,
                                  true /* recursive */,
                                  base::FileEnumerator::DIRECTORIES);
  for (base::FilePath bundle_path = enumerator.Next();
       !bundle_path.empty(); bundle_path = enumerator.Next()) {
    if (IsShimForProfile(bundle_path.BaseName(), profile_base_name))
      bundle_paths.push_back(bundle_path);
  }

  return bundle_paths;
}

scoped_ptr<web_app::ShortcutInfo> BuildShortcutInfoFromBundle(
    const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));

  scoped_ptr<web_app::ShortcutInfo> shortcut_info(new web_app::ShortcutInfo);
  shortcut_info->extension_id = base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeShortcutIDKey]);
  shortcut_info->is_platform_app = true;
  shortcut_info->url = GURL(base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeShortcutURLKey]));
  shortcut_info->title = base::SysNSStringToUTF16(
      [plist valueForKey:app_mode::kCrAppModeShortcutNameKey]);
  shortcut_info->profile_name = base::SysNSStringToUTF8(
      [plist valueForKey:app_mode::kCrAppModeProfileNameKey]);

  // Figure out the profile_path. Since the user_data_dir could contain the
  // path to the web app data dir.
  base::FilePath user_data_dir = base::mac::NSStringToFilePath(
      [plist valueForKey:app_mode::kCrAppModeUserDataDirKey]);
  base::FilePath profile_base_name = base::mac::NSStringToFilePath(
      [plist valueForKey:app_mode::kCrAppModeProfileDirKey]);
  if (user_data_dir.DirName().DirName().BaseName() == profile_base_name)
    shortcut_info->profile_path = user_data_dir.DirName().DirName();
  else
    shortcut_info->profile_path = user_data_dir.Append(profile_base_name);

  return shortcut_info;
}

scoped_ptr<web_app::ShortcutInfo> RecordAppShimErrorAndBuildShortcutInfo(
    const base::FilePath& bundle_path) {
  NSDictionary* plist = ReadPlist(GetPlistPath(bundle_path));
  NSString* version_string = [plist valueForKey:app_mode::kCrBundleVersionKey];
  if (!version_string) {
    // Older bundles have the Chrome version in the following key.
    version_string =
        [plist valueForKey:app_mode::kCFBundleShortVersionStringKey];
  }
  base::Version full_version(base::SysNSStringToUTF8(version_string));
  uint32_t major_version = 0;
  if (full_version.IsValid())
    major_version = full_version.components()[0];
  UMA_HISTOGRAM_SPARSE_SLOWLY("Apps.AppShimErrorVersion", major_version);

  return BuildShortcutInfoFromBundle(bundle_path);
}

void UpdateFileTypes(NSMutableDictionary* plist,
                     const extensions::FileHandlersInfo& file_handlers_info) {
  NSMutableArray* document_types =
      [NSMutableArray arrayWithCapacity:file_handlers_info.size()];

  for (extensions::FileHandlersInfo::const_iterator info_it =
           file_handlers_info.begin();
       info_it != file_handlers_info.end();
       ++info_it) {
    const extensions::FileHandlerInfo& info = *info_it;

    NSMutableArray* file_extensions =
        [NSMutableArray arrayWithCapacity:info.extensions.size()];
    for (std::set<std::string>::iterator it = info.extensions.begin();
         it != info.extensions.end();
         ++it) {
      [file_extensions addObject:base::SysUTF8ToNSString(*it)];
    }

    NSMutableArray* mime_types =
        [NSMutableArray arrayWithCapacity:info.types.size()];
    for (std::set<std::string>::iterator it = info.types.begin();
         it != info.types.end();
         ++it) {
      [mime_types addObject:base::SysUTF8ToNSString(*it)];
    }

    NSDictionary* type_dictionary = @{
      // TODO(jackhou): Add the type name and and icon file once the manifest
      // supports these.
      // app_mode::kCFBundleTypeNameKey : ,
      // app_mode::kCFBundleTypeIconFileKey : ,
      app_mode::kCFBundleTypeExtensionsKey : file_extensions,
      app_mode::kCFBundleTypeMIMETypesKey : mime_types,
      app_mode::kCFBundleTypeRoleKey : app_mode::kBundleTypeRoleViewer
    };
    [document_types addObject:type_dictionary];
  }

  [plist setObject:document_types
            forKey:app_mode::kCFBundleDocumentTypesKey];
}

void RevealAppShimInFinderForAppOnFileThread(
    scoped_ptr<web_app::ShortcutInfo> shortcut_info,
    const base::FilePath& app_path) {
  web_app::WebAppShortcutCreator shortcut_creator(
      app_path, shortcut_info.get(), extensions::FileHandlersInfo());
  shortcut_creator.RevealAppShimInFinder();
}

}  // namespace

@interface CrCreateAppShortcutCheckboxObserver : NSObject {
 @private
  NSButton* checkbox_;
  NSButton* continueButton_;
}

- (id)initWithCheckbox:(NSButton*)checkbox
        continueButton:(NSButton*)continueButton;
- (void)startObserving;
- (void)stopObserving;
@end

@implementation CrCreateAppShortcutCheckboxObserver

- (id)initWithCheckbox:(NSButton*)checkbox
        continueButton:(NSButton*)continueButton {
  if ((self = [super init])) {
    checkbox_ = checkbox;
    continueButton_ = continueButton;
  }
  return self;
}

- (void)startObserving {
  [checkbox_ addObserver:self
              forKeyPath:@"cell.state"
                 options:0
                 context:nil];
}

- (void)stopObserving {
  [checkbox_ removeObserver:self
                 forKeyPath:@"cell.state"];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  [continueButton_ setEnabled:([checkbox_ state] == NSOnState)];
}

@end

namespace web_app {

WebAppShortcutCreator::WebAppShortcutCreator(
    const base::FilePath& app_data_dir,
    const ShortcutInfo* shortcut_info,
    const extensions::FileHandlersInfo& file_handlers_info)
    : app_data_dir_(app_data_dir),
      info_(shortcut_info),
      file_handlers_info_(file_handlers_info) {
  DCHECK(shortcut_info);
}

WebAppShortcutCreator::~WebAppShortcutCreator() {}

base::FilePath WebAppShortcutCreator::GetApplicationsShortcutPath() const {
  base::FilePath applications_dir = GetApplicationsDirname();
  return applications_dir.empty() ?
      base::FilePath() : applications_dir.Append(GetShortcutBasename());
}

base::FilePath WebAppShortcutCreator::GetInternalShortcutPath() const {
  return app_data_dir_.Append(GetShortcutBasename());
}

base::FilePath WebAppShortcutCreator::GetShortcutBasename() const {
  std::string app_name;
  // Check if there should be a separate shortcut made for different profiles.
  // Such shortcuts will have a |profile_name| set on the ShortcutInfo,
  // otherwise it will be empty.
  if (!info_->profile_name.empty()) {
    app_name += info_->profile_path.BaseName().value();
    app_name += ' ';
  }
  app_name += info_->extension_id;
  return base::FilePath(app_name).ReplaceExtension("app");
}

bool WebAppShortcutCreator::BuildShortcut(
    const base::FilePath& staging_path) const {
  // Update the app's plist and icon in a temp directory. This works around
  // a Finder bug where the app's icon doesn't properly update.
  if (!base::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
    LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
               << " failed.";
    return false;
  }

  return UpdatePlist(staging_path) &&
      UpdateDisplayName(staging_path) &&
      UpdateIcon(staging_path);
}

size_t WebAppShortcutCreator::CreateShortcutsIn(
    const std::vector<base::FilePath>& folders) const {
  size_t succeeded = 0;

  base::ScopedTempDir scoped_temp_dir;
  if (!scoped_temp_dir.CreateUniqueTempDir())
    return 0;

  base::FilePath app_name = GetShortcutBasename();
  base::FilePath staging_path = scoped_temp_dir.path().Append(app_name);
  if (!BuildShortcut(staging_path))
    return 0;

  for (std::vector<base::FilePath>::const_iterator it = folders.begin();
       it != folders.end(); ++it) {
    const base::FilePath& dst_path = *it;
    if (!base::CreateDirectory(dst_path)) {
      LOG(ERROR) << "Creating directory " << dst_path.value() << " failed.";
      return succeeded;
    }

    // Ensure the copy does not merge with stale info.
    base::DeleteFile(dst_path.Append(app_name), true);

    if (!base::CopyDirectory(staging_path, dst_path, true)) {
      LOG(ERROR) << "Copying app to dst path: " << dst_path.value()
                 << " failed";
      return succeeded;
    }

    // Remove the quarantine attribute from both the bundle and the executable.
    base::mac::RemoveQuarantineAttribute(dst_path.Append(app_name));
    base::mac::RemoveQuarantineAttribute(
        dst_path.Append(app_name)
            .Append("Contents").Append("MacOS").Append("app_mode_loader"));
    ++succeeded;
  }

  return succeeded;
}

bool WebAppShortcutCreator::CreateShortcuts(
    ShortcutCreationReason creation_reason,
    ShortcutLocations creation_locations) {
  const base::FilePath applications_dir = GetApplicationsDirname();
  if (applications_dir.empty() ||
      !base::DirectoryExists(applications_dir.DirName())) {
    LOG(ERROR) << "Couldn't find an Applications directory to copy app to.";
    return false;
  }

  UpdateAppShortcutsSubdirLocalizedName(applications_dir);

  // If non-nil, this path is added to the OSX Dock after creating shortcuts.
  NSString* path_to_add_to_dock = nil;

  std::vector<base::FilePath> paths;

  // The app list shim is not tied to a particular profile, so omit the copy
  // placed under the profile path. For shims, this copy is used when the
  // version under Applications is removed, and not needed for app list because
  // setting LSUIElement means there is no Dock "running" status to show.
  const bool is_app_list = info_->extension_id == app_mode::kAppListModeId;
  if (is_app_list) {
    path_to_add_to_dock = base::SysUTF8ToNSString(
        applications_dir.Append(GetShortcutBasename()).AsUTF8Unsafe());
  } else {
    paths.push_back(app_data_dir_);
  }

  bool shortcut_visible =
      creation_locations.applications_menu_location != APP_MENU_LOCATION_HIDDEN;
  if (shortcut_visible)
    paths.push_back(applications_dir);

  DCHECK(!paths.empty());
  size_t success_count = CreateShortcutsIn(paths);
  if (success_count == 0)
    return false;

  if (!is_app_list)
    UpdateInternalBundleIdentifier();

  if (success_count != paths.size())
    return false;

  if (creation_locations.in_quick_launch_bar && path_to_add_to_dock &&
      shortcut_visible) {
    switch (dock::AddIcon(path_to_add_to_dock, nil)) {
      case dock::IconAddFailure:
        // If adding the icon failed, instead reveal the Finder window.
        RevealAppShimInFinder();
        break;
      case dock::IconAddSuccess:
      case dock::IconAlreadyPresent:
        break;
    }
    return true;
  }

  if (creation_reason == SHORTCUT_CREATION_BY_USER)
    RevealAppShimInFinder();

  return true;
}

void WebAppShortcutCreator::DeleteShortcuts() {
  base::FilePath app_path = GetApplicationsShortcutPath();
  if (!app_path.empty() && HasSameUserDataDir(app_path))
    DeletePathAndParentIfEmpty(app_path);

  // In case the user has moved/renamed/copied the app bundle.
  base::FilePath bundle_path = GetAppBundleById(GetBundleIdentifier());
  if (!bundle_path.empty() && HasSameUserDataDir(bundle_path))
    base::DeleteFile(bundle_path, true);

  // Delete the internal one.
  DeletePathAndParentIfEmpty(GetInternalShortcutPath());
}

bool WebAppShortcutCreator::UpdateShortcuts() {
  std::vector<base::FilePath> paths;
  paths.push_back(app_data_dir_);

  // Try to update the copy under /Applications. If that does not exist, check
  // if a matching bundle can be found elsewhere.
  base::FilePath app_path = GetApplicationsShortcutPath();
  if (app_path.empty() || !base::PathExists(app_path))
    app_path = GetAppBundleById(GetBundleIdentifier());

  if (!app_path.empty())
    paths.push_back(app_path.DirName());

  size_t success_count = CreateShortcutsIn(paths);
  if (success_count == 0)
    return false;

  UpdateInternalBundleIdentifier();
  return success_count == paths.size() && !app_path.empty();
}

base::FilePath WebAppShortcutCreator::GetApplicationsDirname() const {
  base::FilePath path = GetWritableApplicationsDirectory();
  if (path.empty())
    return path;

  return path.Append(GetLocalizableAppShortcutsSubdirName());
}

bool WebAppShortcutCreator::UpdatePlist(const base::FilePath& app_path) const {
  NSString* extension_id = base::SysUTF8ToNSString(info_->extension_id);
  NSString* extension_title = base::SysUTF16ToNSString(info_->title);
  NSString* extension_url = base::SysUTF8ToNSString(info_->url.spec());
  NSString* chrome_bundle_id =
      base::SysUTF8ToNSString(base::mac::BaseBundleID());
  NSDictionary* replacement_dict =
      [NSDictionary dictionaryWithObjectsAndKeys:
          extension_id, app_mode::kShortcutIdPlaceholder,
          extension_title, app_mode::kShortcutNamePlaceholder,
          extension_url, app_mode::kShortcutURLPlaceholder,
          chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
          nil];

  NSString* plist_path = GetPlistPath(app_path);
  NSMutableDictionary* plist = ReadPlist(plist_path);
  NSArray* keys = [plist allKeys];

  // 1. Fill in variables.
  for (id key in keys) {
    NSString* value = [plist valueForKey:key];
    if (![value isKindOfClass:[NSString class]] || [value length] < 2)
      continue;

    // Remove leading and trailing '@'s.
    NSString* variable =
        [value substringWithRange:NSMakeRange(1, [value length] - 2)];

    NSString* substitution = [replacement_dict valueForKey:variable];
    if (substitution)
      [plist setObject:substitution forKey:key];
  }

  // 2. Fill in other values.
  [plist setObject:base::SysUTF8ToNSString(chrome::VersionInfo().Version())
            forKey:app_mode::kCrBundleVersionKey];
  [plist setObject:base::SysUTF8ToNSString(info_->version_for_display)
            forKey:app_mode::kCFBundleShortVersionStringKey];
  [plist setObject:base::SysUTF8ToNSString(GetBundleIdentifier())
            forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
  [plist setObject:base::mac::FilePathToNSString(app_data_dir_)
            forKey:app_mode::kCrAppModeUserDataDirKey];
  [plist setObject:base::mac::FilePathToNSString(info_->profile_path.BaseName())
            forKey:app_mode::kCrAppModeProfileDirKey];
  [plist setObject:base::SysUTF8ToNSString(info_->profile_name)
            forKey:app_mode::kCrAppModeProfileNameKey];
  [plist setObject:[NSNumber numberWithBool:YES]
            forKey:app_mode::kLSHasLocalizedDisplayNameKey];
  if (info_->extension_id == app_mode::kAppListModeId) {
    // Prevent the app list from bouncing in the dock, and getting a run light.
    [plist setObject:[NSNumber numberWithBool:YES]
              forKey:kLSUIElement];
  }

  base::FilePath app_name = app_path.BaseName().RemoveExtension();
  [plist setObject:base::mac::FilePathToNSString(app_name)
            forKey:base::mac::CFToNSCast(kCFBundleNameKey)];

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kEnableAppsFileAssociations)) {
    UpdateFileTypes(plist, file_handlers_info_);
  }

  return [plist writeToFile:plist_path
                 atomically:YES];
}

bool WebAppShortcutCreator::UpdateDisplayName(
    const base::FilePath& app_path) const {
  // Localization is used to display the app name (rather than the bundle
  // filename). OSX searches for the best language in the order of preferred
  // languages, but one of them must be found otherwise it will default to
  // the filename.
  NSString* language = [[NSLocale preferredLanguages] objectAtIndex:0];
  base::FilePath localized_dir = GetResourcesPath(app_path).Append(
      base::SysNSStringToUTF8(language) + ".lproj");
  if (!base::CreateDirectory(localized_dir))
    return false;

  NSString* bundle_name = base::SysUTF16ToNSString(info_->title);
  NSString* display_name = base::SysUTF16ToNSString(info_->title);
  if (HasExistingExtensionShim(GetApplicationsDirname(), info_->extension_id,
                               app_path.BaseName())) {
    display_name = [bundle_name
        stringByAppendingString:base::SysUTF8ToNSString(
            " (" + info_->profile_name + ")")];
  }

  NSDictionary* strings_plist = @{
    base::mac::CFToNSCast(kCFBundleNameKey) : bundle_name,
    app_mode::kCFBundleDisplayNameKey : display_name
  };
{'fraction_non_alphanumeric': 0.11067889908256881, 'fraction_numerical': 0.0042568807339449545, 'mean_word_length': 4.341121128969032, 'pattern_counts': {'":': 0, '<': 93, '<?xml version=': 0, '>': 125, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6605187', 'n_tokens_mistral': 21621, 'n_tokens_neox': 20809, 'n_words': 4085}