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.
5 #import "chrome/browser/web_applications/web_app_mac.h"
7 #import <Cocoa/Cocoa.h>
9 #include "base/file_util.h"
10 #include "base/mac/bundle_locations.h"
11 #include "base/mac/foundation_util.h"
12 #include "base/mac/mac_util.h"
13 #include "base/memory/scoped_nsobject.h"
14 #include "base/scoped_temp_dir.h"
15 #include "base/sys_string_conversions.h"
16 #include "base/utf_string_conversions.h"
17 #include "chrome/browser/web_applications/web_app.h"
18 #include "chrome/common/chrome_paths_internal.h"
19 #include "chrome/common/mac/app_mode_common.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "grit/chromium_strings.h"
22 #include "skia/ext/skia_utils_mac.h"
23 #include "third_party/icon_family/IconFamily.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
25 #include "ui/gfx/image/image_skia.h"
29 // Creates a NSBitmapImageRep from |bitmap|.
30 NSBitmapImageRep* SkBitmapToImageRep(const SkBitmap& bitmap) {
31 base::mac::ScopedCFTypeRef<CGColorSpaceRef> color_space(
32 CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB));
33 NSImage* image = gfx::SkBitmapToNSImageWithColorSpace(
34 bitmap, color_space.get());
35 return base::mac::ObjCCast<NSBitmapImageRep>(
36 [[image representations] lastObject]);
39 // Adds |image_rep| to |icon_family|. Returns true on success, false on failure.
40 bool AddBitmapImageRepToIconFamily(IconFamily* icon_family,
41 NSBitmapImageRep* image_rep) {
42 NSSize size = [image_rep size];
43 if (size.width != size.height)
46 switch (static_cast<int>(size.width)) {
48 return [icon_family setIconFamilyElement:kIconServices512PixelDataARGB
49 fromBitmapImageRep:image_rep];
51 return [icon_family setIconFamilyElement:kIconServices256PixelDataARGB
52 fromBitmapImageRep:image_rep];
54 return [icon_family setIconFamilyElement:kThumbnail32BitData
55 fromBitmapImageRep:image_rep] &&
56 [icon_family setIconFamilyElement:kThumbnail8BitMask
57 fromBitmapImageRep:image_rep];
59 return [icon_family setIconFamilyElement:kLarge32BitData
60 fromBitmapImageRep:image_rep] &&
61 [icon_family setIconFamilyElement:kLarge8BitData
62 fromBitmapImageRep:image_rep] &&
63 [icon_family setIconFamilyElement:kLarge8BitMask
64 fromBitmapImageRep:image_rep] &&
65 [icon_family setIconFamilyElement:kLarge1BitMask
66 fromBitmapImageRep:image_rep];
68 return [icon_family setIconFamilyElement:kSmall32BitData
69 fromBitmapImageRep:image_rep] &&
70 [icon_family setIconFamilyElement:kSmall8BitData
71 fromBitmapImageRep:image_rep] &&
72 [icon_family setIconFamilyElement:kSmall8BitMask
73 fromBitmapImageRep:image_rep] &&
74 [icon_family setIconFamilyElement:kSmall1BitMask
75 fromBitmapImageRep:image_rep];
86 WebAppShortcutCreator::WebAppShortcutCreator(
87 const FilePath& user_data_dir,
88 const ShellIntegration::ShortcutInfo& shortcut_info,
89 const string16& chrome_bundle_id)
90 : user_data_dir_(user_data_dir),
92 chrome_bundle_id_(chrome_bundle_id) {
95 WebAppShortcutCreator::~WebAppShortcutCreator() {
98 bool WebAppShortcutCreator::CreateShortcut() {
99 FilePath app_name = internals::GetSanitizedFileName(info_.title);
100 FilePath app_file_name = app_name.ReplaceExtension("app");
101 ScopedTempDir scoped_temp_dir;
102 if (!scoped_temp_dir.CreateUniqueTempDir())
104 FilePath staging_path = scoped_temp_dir.path().Append(app_file_name);
106 // Update the app's plist and icon in a temp directory. This works around
107 // a Finder bug where the app's icon doesn't properly update.
108 if (!file_util::CopyDirectory(GetAppLoaderPath(), staging_path, true)) {
109 LOG(ERROR) << "Copying app to staging path: " << staging_path.value()
114 if (!UpdatePlist(staging_path))
117 if (!UpdateIcon(staging_path))
120 FilePath dst_path = GetDestinationPath(app_file_name);
121 if (!file_util::CopyDirectory(staging_path, dst_path, true)) {
122 LOG(ERROR) << "Copying app to dst path: " << dst_path.value() << " failed";
126 RevealGeneratedBundleInFinder(dst_path);
131 FilePath WebAppShortcutCreator::GetAppLoaderPath() const {
132 return base::mac::PathForFrameworkBundleResource(
133 base::mac::NSToCFCast(@"app_mode_loader.app"));
136 FilePath WebAppShortcutCreator::GetDestinationPath(
137 const FilePath& app_file_name) const {
139 if (base::mac::GetLocalDirectory(NSApplicationDirectory, &path) &&
140 file_util::PathIsWritable(path)) {
144 if (base::mac::GetUserDirectory(NSApplicationDirectory, &path))
150 bool WebAppShortcutCreator::UpdatePlist(const FilePath& app_path) const {
151 NSString* plist_path = base::mac::FilePathToNSString(
152 app_path.Append("Contents").Append("Info.plist"));
154 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
155 NSString* extension_title = base::SysUTF16ToNSString(info_.title);
156 NSString* extension_url = base::SysUTF8ToNSString(info_.url.spec());
157 NSString* chrome_bundle_id = base::SysUTF16ToNSString(chrome_bundle_id_);
158 NSDictionary* replacement_dict =
159 [NSDictionary dictionaryWithObjectsAndKeys:
160 extension_id, app_mode::kShortcutIdPlaceholder,
161 extension_title, app_mode::kShortcutNamePlaceholder,
162 extension_url, app_mode::kShortcutURLPlaceholder,
163 chrome_bundle_id, app_mode::kShortcutBrowserBundleIDPlaceholder,
166 NSMutableDictionary* plist =
167 [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
168 NSArray* keys = [plist allKeys];
170 // 1. Fill in variables.
171 for (id key in keys) {
172 NSString* value = [plist valueForKey:key];
173 if (![value isKindOfClass:[NSString class]] || [value length] < 2)
176 // Remove leading and trailing '@'s.
178 [value substringWithRange:NSMakeRange(1, [value length] - 2)];
180 NSString* substitution = [replacement_dict valueForKey:variable];
182 [plist setObject:substitution forKey:key];
185 // 2. Fill in other values.
186 [plist setObject:GetBundleIdentifier(plist)
187 forKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
188 [plist setObject:base::mac::FilePathToNSString(user_data_dir_)
189 forKey:app_mode::kCrAppModeUserDataDirKey];
190 [plist setObject:base::mac::FilePathToNSString(info_.extension_path)
191 forKey:app_mode::kCrAppModeExtensionPathKey];
192 return [plist writeToFile:plist_path atomically:YES];
195 bool WebAppShortcutCreator::UpdateIcon(const FilePath& app_path) const {
196 if (info_.favicon.IsEmpty())
199 scoped_nsobject<IconFamily> icon_family([[IconFamily alloc] init]);
200 bool image_added = false;
201 info_.favicon.ToImageSkia()->EnsureRepsForSupportedScaleFactors();
202 std::vector<gfx::ImageSkiaRep> image_reps =
203 info_.favicon.ToImageSkia()->image_reps();
204 for (size_t i = 0; i < image_reps.size(); ++i) {
205 NSBitmapImageRep* image_rep = SkBitmapToImageRep(
206 image_reps[i].sk_bitmap());
210 // Missing an icon size is not fatal so don't fail if adding the bitmap
212 if (!AddBitmapImageRepToIconFamily(icon_family, image_rep))
221 FilePath resources_path = app_path.Append("Contents").Append("Resources");
222 if (!file_util::CreateDirectory(resources_path))
224 FilePath icon_path = resources_path.Append("app.icns");
225 return [icon_family writeToFile:base::mac::FilePathToNSString(icon_path)];
228 NSString* WebAppShortcutCreator::GetBundleIdentifier(NSDictionary* plist) const
230 NSString* bundle_id_template =
231 base::mac::ObjCCast<NSString>(
232 [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)]);
233 NSString* extension_id = base::SysUTF8ToNSString(info_.extension_id);
234 NSString* placeholder =
235 [NSString stringWithFormat:@"@%@@", app_mode::kShortcutIdPlaceholder];
236 NSString* bundle_id =
238 stringByReplacingOccurrencesOfString:placeholder
239 withString:extension_id];
243 void WebAppShortcutCreator::RevealGeneratedBundleInFinder(
244 const FilePath& generated_bundle) const {
245 [[NSWorkspace sharedWorkspace]
246 selectFile:base::mac::FilePathToNSString(generated_bundle)
247 inFileViewerRootedAtPath:nil];
254 namespace internals {
256 bool CreatePlatformShortcuts(
257 const FilePath& web_app_path,
258 const ShellIntegration::ShortcutInfo& shortcut_info) {
259 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
260 string16 bundle_id = UTF8ToUTF16(base::mac::BaseBundleID());
261 WebAppShortcutCreator shortcut_creator(web_app_path, shortcut_info,
263 return shortcut_creator.CreateShortcut();
266 void DeletePlatformShortcuts(
267 const FilePath& web_app_path,
268 const ShellIntegration::ShortcutInfo& shortcut_info) {
269 // TODO(benwells): Implement this when shortcuts / weblings are enabled on
273 void UpdatePlatformShortcuts(
274 const FilePath& web_app_path,
275 const ShellIntegration::ShortcutInfo& shortcut_info) {
276 // TODO(benwells): Implement this when shortcuts / weblings are enabled on
280 } // namespace internals
282 } // namespace web_app