1// Copyright (c) 2011 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#include "chrome/browser/ui/web_applications/web_app_ui.h"
6
7#include "base/file_util.h"
8#include "base/path_service.h"
9#include "base/task.h"
10#include "base/win/windows_version.h"
11#include "chrome/browser/extensions/extension_tab_helper.h"
12#include "chrome/browser/profiles/profile.h"
13#include "chrome/browser/web_applications/web_app.h"
14#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
15#include "chrome/common/chrome_paths.h"
16#include "content/browser/browser_thread.h"
17#include "content/browser/tab_contents/tab_contents.h"
18#include "content/common/notification_registrar.h"
19
20#if defined(OS_LINUX)
21#include "base/environment.h"
22#endif  // defined(OS_LINUX)
23
24#if defined(OS_WIN)
25#include "content/common/notification_details.h"
26#include "content/common/notification_source.h"
27#endif  // defined(OS_WIN)
28
29namespace {
30
31#if defined(OS_WIN)
32// UpdateShortcutWorker holds all context data needed for update shortcut.
33// It schedules a pre-update check to find all shortcuts that needs to be
34// updated. If there are such shortcuts, it schedules icon download and
35// update them when icons are downloaded. It observes TAB_CLOSING notification
36// and cancels all the work when the underlying tab is closing.
37class UpdateShortcutWorker : public NotificationObserver {
38 public:
39  explicit UpdateShortcutWorker(TabContentsWrapper* tab_contents);
40
41  void Run();
42
43 private:
44  // Overridden from NotificationObserver:
45  virtual void Observe(NotificationType type,
46                       const NotificationSource& source,
47                       const NotificationDetails& details);
48
49  // Downloads icon via TabContents.
50  void DownloadIcon();
51
52  // Callback when icon downloaded.
53  void OnIconDownloaded(int download_id, bool errored, const SkBitmap& image);
54
55  // Checks if shortcuts exists on desktop, start menu and quick launch.
56  void CheckExistingShortcuts();
57
58  // Update shortcut files and icons.
59  void UpdateShortcuts();
60  void UpdateShortcutsOnFileThread();
61
62  // Callback after shortcuts are updated.
63  void OnShortcutsUpdated(bool);
64
65  // Deletes the worker on UI thread where it gets created.
66  void DeleteMe();
67  void DeleteMeOnUIThread();
68
69  NotificationRegistrar registrar_;
70
71  // Underlying TabContentsWrapper whose shortcuts will be updated.
72  TabContentsWrapper* tab_contents_;
73
74  // Icons info from tab_contents_'s web app data.
75  web_app::IconInfoList unprocessed_icons_;
76
77  // Cached shortcut data from the tab_contents_.
78  ShellIntegration::ShortcutInfo shortcut_info_;
79
80  // Our copy of profile path.
81  FilePath profile_path_;
82
83  // File name of shortcut/ico file based on app title.
84  FilePath file_name_;
85
86  // Existing shortcuts.
87  std::vector<FilePath> shortcut_files_;
88
89  DISALLOW_COPY_AND_ASSIGN(UpdateShortcutWorker);
90};
91
92UpdateShortcutWorker::UpdateShortcutWorker(TabContentsWrapper* tab_contents)
93    : tab_contents_(tab_contents),
94      profile_path_(tab_contents->profile()->GetPath()) {
95  web_app::GetShortcutInfoForTab(tab_contents_, &shortcut_info_);
96  web_app::GetIconsInfo(tab_contents_->extension_tab_helper()->web_app_info(),
97                        &unprocessed_icons_);
98  file_name_ = web_app::internals::GetSanitizedFileName(shortcut_info_.title);
99
100  registrar_.Add(this, NotificationType::TAB_CLOSING,
101                 Source<NavigationController>(&tab_contents_->controller()));
102}
103
104void UpdateShortcutWorker::Run() {
105  // Starting by downloading app icon.
106  DownloadIcon();
107}
108
109void UpdateShortcutWorker::Observe(NotificationType type,
110                                   const NotificationSource& source,
111                                   const NotificationDetails& details) {
112  if (type == NotificationType::TAB_CLOSING &&
113      Source<NavigationController>(source).ptr() ==
114        &tab_contents_->controller()) {
115    // Underlying tab is closing.
116    tab_contents_ = NULL;
117  }
118}
119
120void UpdateShortcutWorker::DownloadIcon() {
121  // FetchIcon must run on UI thread because it relies on TabContents
122  // to download the icon.
123  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
124
125  if (tab_contents_ == NULL) {
126    DeleteMe();  // We are done if underlying TabContents is gone.
127    return;
128  }
129
130  if (unprocessed_icons_.empty()) {
131    // No app icon. Just use the favicon from TabContents.
132    UpdateShortcuts();
133    return;
134  }
135
136  tab_contents_->tab_contents()->favicon_helper().DownloadImage(
137      unprocessed_icons_.back().url,
138      std::max(unprocessed_icons_.back().width,
139               unprocessed_icons_.back().height),
140      history::FAVICON,
141      NewCallback(this, &UpdateShortcutWorker::OnIconDownloaded));
142  unprocessed_icons_.pop_back();
143}
144
145void UpdateShortcutWorker::OnIconDownloaded(int download_id,
146                                            bool errored,
147                                            const SkBitmap& image) {
148  if (tab_contents_ == NULL) {
149    DeleteMe();  // We are done if underlying TabContents is gone.
150    return;
151  }
152
153  if (!errored && !image.isNull()) {
154    // Update icon with download image and update shortcut.
155    shortcut_info_.favicon = image;
156    tab_contents_->extension_tab_helper()->SetAppIcon(image);
157    UpdateShortcuts();
158  } else {
159    // Try the next icon otherwise.
160    DownloadIcon();
161  }
162}
163
164void UpdateShortcutWorker::CheckExistingShortcuts() {
165  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
166
167  // Locations to check to shortcut_paths.
168  struct {
169    bool& use_this_location;
170    int location_id;
171    const wchar_t* sub_dir;
172  } locations[] = {
173    {
174      shortcut_info_.create_on_desktop,
175      chrome::DIR_USER_DESKTOP,
176      NULL
177    }, {
178      shortcut_info_.create_in_applications_menu,
179      base::DIR_START_MENU,
180      NULL
181    }, {
182      shortcut_info_.create_in_quick_launch_bar,
183      // For Win7, create_in_quick_launch_bar means pinning to taskbar.
184      base::DIR_APP_DATA,
185      (base::win::GetVersion() >= base::win::VERSION_WIN7) ?
186          L"Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar" :
187          L"Microsoft\\Internet Explorer\\Quick Launch"
188    }
189  };
190
191  for (int i = 0; i < arraysize(locations); ++i) {
192    locations[i].use_this_location = false;
193
194    FilePath path;
195    if (!PathService::Get(locations[i].location_id, &path)) {
196      NOTREACHED();
197      continue;
198    }
199
200    if (locations[i].sub_dir != NULL)
201      path = path.Append(locations[i].sub_dir);
202
203    FilePath shortcut_file = path.Append(file_name_).
204        ReplaceExtension(FILE_PATH_LITERAL(".lnk"));
205    if (file_util::PathExists(shortcut_file)) {
206      locations[i].use_this_location = true;
207      shortcut_files_.push_back(shortcut_file);
208    }
209  }
210}
211
212void UpdateShortcutWorker::UpdateShortcuts() {
213  BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
214      NewRunnableMethod(this,
215      &UpdateShortcutWorker::UpdateShortcutsOnFileThread));
216}
217
218void UpdateShortcutWorker::UpdateShortcutsOnFileThread() {
219  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
220
221  FilePath web_app_path = web_app::internals::GetWebAppDataDirectory(
222      web_app::GetDataDir(profile_path_), shortcut_info_);
223
224  // Ensure web_app_path exists. web_app_path could be missing for a legacy
225  // shortcut created by Gears.
226  if (!file_util::PathExists(web_app_path) &&
227      !file_util::CreateDirectory(web_app_path)) {
228    NOTREACHED();
229    return;
230  }
231
232  FilePath icon_file = web_app_path.Append(file_name_).ReplaceExtension(
233      FILE_PATH_LITERAL(".ico"));
234  web_app::internals::CheckAndSaveIcon(icon_file, shortcut_info_.favicon);
235
236  // Update existing shortcuts' description, icon and app id.
237  CheckExistingShortcuts();
238  if (!shortcut_files_.empty()) {
239    // Generates app id from web app url and profile path.
240    std::wstring app_id = ShellIntegration::GetAppId(
241        UTF8ToWide(web_app::GenerateApplicationNameFromURL(shortcut_info_.url)),
242        profile_path_);
243
244    // Sanitize description
245    if (shortcut_info_.description.length() >= MAX_PATH)
246      shortcut_info_.description.resize(MAX_PATH - 1);
247
248    for (size_t i = 0; i < shortcut_files_.size(); ++i) {
249      file_util::UpdateShortcutLink(NULL,
250          shortcut_files_[i].value().c_str(),
251          NULL,
252          NULL,
253          shortcut_info_.description.c_str(),
254          icon_file.value().c_str(),
255          0,
256          app_id.c_str());
257    }
258  }
259
260  OnShortcutsUpdated(true);
261}
262
263void UpdateShortcutWorker::OnShortcutsUpdated(bool) {
264  DeleteMe();  // We are done.
265}
266
267void UpdateShortcutWorker::DeleteMe() {
268  if (BrowserThread::CurrentlyOn(BrowserThread::UI)) {
269    DeleteMeOnUIThread();
270  } else {
271    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
272      NewRunnableMethod(this, &UpdateShortcutWorker::DeleteMeOnUIThread));
273  }
274}
275
276void UpdateShortcutWorker::DeleteMeOnUIThread() {
277  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
278  delete this;
279}
280#endif  // defined(OS_WIN)
281
282}  // namespace
283
284#if defined(OS_WIN)
285// Allows UpdateShortcutWorker without adding refcounting. UpdateShortcutWorker
286// manages its own life time and will delete itself when it's done.
287DISABLE_RUNNABLE_METHOD_REFCOUNT(UpdateShortcutWorker);
288#endif  // defined(OS_WIN)
289
290namespace web_app {
291
292void GetShortcutInfoForTab(TabContentsWrapper* tab_contents_wrapper,
293                           ShellIntegration::ShortcutInfo* info) {
294  DCHECK(info);  // Must provide a valid info.
295  const TabContents* tab_contents = tab_contents_wrapper->tab_contents();
296
297  const WebApplicationInfo& app_info =
298      tab_contents_wrapper->extension_tab_helper()->web_app_info();
299
300  info->url = app_info.app_url.is_empty() ? tab_contents->GetURL() :
301                                            app_info.app_url;
302  info->title = app_info.title.empty() ?
303      (tab_contents->GetTitle().empty() ? UTF8ToUTF16(info->url.spec()) :
304                                          tab_contents->GetTitle()) :
305      app_info.title;
306  info->description = app_info.description;
307  info->favicon = tab_contents->GetFavicon();
308}
309
310void UpdateShortcutForTabContents(TabContentsWrapper* tab_contents) {
311#if defined(OS_WIN)
312  // UpdateShortcutWorker will delete itself when it's done.
313  UpdateShortcutWorker* worker = new UpdateShortcutWorker(tab_contents);
314  worker->Run();
315#endif  // defined(OS_WIN)
316}
317
318}  // namespace web_app
319