1// Copyright 2013 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/libgtk2ui/app_indicator_icon.h"
6
7#include <dlfcn.h>
8#include <gtk/gtk.h>
9
10#include "base/bind.h"
11#include "base/environment.h"
12#include "base/files/file_util.h"
13#include "base/md5.h"
14#include "base/memory/ref_counted_memory.h"
15#include "base/nix/xdg_util.h"
16#include "base/strings/stringprintf.h"
17#include "base/strings/utf_string_conversions.h"
18#include "base/threading/sequenced_worker_pool.h"
19#include "chrome/browser/ui/libgtk2ui/app_indicator_icon_menu.h"
20#include "content/public/browser/browser_thread.h"
21#include "ui/base/models/menu_model.h"
22#include "ui/gfx/image/image.h"
23#include "ui/gfx/image/image_skia.h"
24
25namespace {
26
27typedef enum {
28  APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
29  APP_INDICATOR_CATEGORY_COMMUNICATIONS,
30  APP_INDICATOR_CATEGORY_SYSTEM_SERVICES,
31  APP_INDICATOR_CATEGORY_HARDWARE,
32  APP_INDICATOR_CATEGORY_OTHER
33} AppIndicatorCategory;
34
35typedef enum {
36  APP_INDICATOR_STATUS_PASSIVE,
37  APP_INDICATOR_STATUS_ACTIVE,
38  APP_INDICATOR_STATUS_ATTENTION
39} AppIndicatorStatus;
40
41typedef AppIndicator* (*app_indicator_new_func)(const gchar* id,
42                                                const gchar* icon_name,
43                                                AppIndicatorCategory category);
44
45typedef AppIndicator* (*app_indicator_new_with_path_func)(
46    const gchar* id,
47    const gchar* icon_name,
48    AppIndicatorCategory category,
49    const gchar* icon_theme_path);
50
51typedef void (*app_indicator_set_status_func)(AppIndicator* self,
52                                              AppIndicatorStatus status);
53
54typedef void (*app_indicator_set_attention_icon_full_func)(
55    AppIndicator* self,
56    const gchar* icon_name,
57    const gchar* icon_desc);
58
59typedef void (*app_indicator_set_menu_func)(AppIndicator* self, GtkMenu* menu);
60
61typedef void (*app_indicator_set_icon_full_func)(AppIndicator* self,
62                                                 const gchar* icon_name,
63                                                 const gchar* icon_desc);
64
65typedef void (*app_indicator_set_icon_theme_path_func)(
66    AppIndicator* self,
67    const gchar* icon_theme_path);
68
69bool g_attempted_load = false;
70bool g_opened = false;
71
72// Retrieved functions from libappindicator.
73app_indicator_new_func app_indicator_new = NULL;
74app_indicator_new_with_path_func app_indicator_new_with_path = NULL;
75app_indicator_set_status_func app_indicator_set_status = NULL;
76app_indicator_set_attention_icon_full_func
77    app_indicator_set_attention_icon_full = NULL;
78app_indicator_set_menu_func app_indicator_set_menu = NULL;
79app_indicator_set_icon_full_func app_indicator_set_icon_full = NULL;
80app_indicator_set_icon_theme_path_func app_indicator_set_icon_theme_path = NULL;
81
82void EnsureMethodsLoaded() {
83  if (g_attempted_load)
84    return;
85
86  g_attempted_load = true;
87
88  // Only use libappindicator where it is needed to support dbus based status
89  // icons. In particular, libappindicator does not support a click action.
90  scoped_ptr<base::Environment> env(base::Environment::Create());
91  base::nix::DesktopEnvironment environment =
92      base::nix::GetDesktopEnvironment(env.get());
93  if (environment != base::nix::DESKTOP_ENVIRONMENT_KDE4 &&
94      environment != base::nix::DESKTOP_ENVIRONMENT_UNITY) {
95    return;
96  }
97
98  void* indicator_lib = dlopen("libappindicator.so", RTLD_LAZY);
99  if (!indicator_lib) {
100    indicator_lib = dlopen("libappindicator.so.1", RTLD_LAZY);
101  }
102  if (!indicator_lib) {
103    indicator_lib = dlopen("libappindicator.so.0", RTLD_LAZY);
104  }
105  if (!indicator_lib) {
106    return;
107  }
108
109  g_opened = true;
110
111  app_indicator_new = reinterpret_cast<app_indicator_new_func>(
112      dlsym(indicator_lib, "app_indicator_new"));
113
114  app_indicator_new_with_path =
115      reinterpret_cast<app_indicator_new_with_path_func>(
116          dlsym(indicator_lib, "app_indicator_new_with_path"));
117
118  app_indicator_set_status = reinterpret_cast<app_indicator_set_status_func>(
119      dlsym(indicator_lib, "app_indicator_set_status"));
120
121  app_indicator_set_attention_icon_full =
122      reinterpret_cast<app_indicator_set_attention_icon_full_func>(
123          dlsym(indicator_lib, "app_indicator_set_attention_icon_full"));
124
125  app_indicator_set_menu = reinterpret_cast<app_indicator_set_menu_func>(
126      dlsym(indicator_lib, "app_indicator_set_menu"));
127
128  app_indicator_set_icon_full =
129      reinterpret_cast<app_indicator_set_icon_full_func>(
130          dlsym(indicator_lib, "app_indicator_set_icon_full"));
131
132  app_indicator_set_icon_theme_path =
133      reinterpret_cast<app_indicator_set_icon_theme_path_func>(
134          dlsym(indicator_lib, "app_indicator_set_icon_theme_path"));
135}
136
137// Returns whether a temporary directory should be created for each app
138// indicator image.
139bool ShouldCreateTempDirectoryPerImage(bool using_kde4) {
140  // Create a new temporary directory for each image on Unity since using a
141  // single temporary directory seems to have issues when changing icons in
142  // quick succession.
143  return !using_kde4;
144}
145
146// Returns the subdirectory of |temp_dir| in which the app indicator image
147// should be saved.
148base::FilePath GetImageDirectoryPath(bool using_kde4,
149                                     const base::FilePath& temp_dir) {
150  // On KDE4, an image located in a directory ending with
151  // "icons/hicolor/16x16/apps" can be used as the app indicator image because
152  // "/usr/share/icons/hicolor/16x16/apps" exists.
153  return using_kde4 ?
154      temp_dir.AppendASCII("icons").AppendASCII("hicolor").AppendASCII("16x16").
155          AppendASCII("apps") :
156      temp_dir;
157}
158
159std::string GetImageFileNameForKDE4(
160    const scoped_refptr<base::RefCountedMemory>& png_data) {
161  // On KDE4, the name of the image file for each different looking bitmap must
162  // be unique. It must also be unique across runs of Chrome.
163  base::MD5Digest digest;
164  base::MD5Sum(png_data->front_as<char>(), png_data->size(), &digest);
165  return base::StringPrintf("chrome_app_indicator_%s.png",
166                            base::MD5DigestToBase16(digest).c_str());
167}
168
169std::string GetImageFileNameForNonKDE4(int icon_change_count,
170                                       const std::string& id) {
171  return base::StringPrintf("%s_%d.png", id.c_str(), icon_change_count);
172}
173
174// Returns the "icon theme path" given the file path of the app indicator image.
175std::string GetIconThemePath(bool using_kde4,
176                             const base::FilePath& image_path) {
177  return using_kde4 ?
178      image_path.DirName().DirName().DirName().DirName().value() :
179      image_path.DirName().value();
180}
181
182base::FilePath CreateTempImageFile(bool using_kde4,
183                                   gfx::ImageSkia* image_ptr,
184                                   int icon_change_count,
185                                   std::string id,
186                                   const base::FilePath& previous_file_path) {
187  scoped_ptr<gfx::ImageSkia> image(image_ptr);
188
189  scoped_refptr<base::RefCountedMemory> png_data =
190      gfx::Image(*image.get()).As1xPNGBytes();
191  if (png_data->size() == 0) {
192    // If the bitmap could not be encoded to PNG format, skip it.
193    LOG(WARNING) << "Could not encode icon";
194    return base::FilePath();
195  }
196
197  base::FilePath new_file_path;
198  if (previous_file_path.empty() ||
199      ShouldCreateTempDirectoryPerImage(using_kde4)) {
200    base::FilePath tmp_dir;
201    if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &tmp_dir))
202      return base::FilePath();
203    new_file_path = GetImageDirectoryPath(using_kde4, tmp_dir);
204    if (new_file_path != tmp_dir) {
205      if (!base::CreateDirectory(new_file_path))
206        return base::FilePath();
207    }
208  } else {
209    new_file_path = previous_file_path.DirName();
210  }
211
212  new_file_path = new_file_path.Append(using_kde4 ?
213      GetImageFileNameForKDE4(png_data) :
214      GetImageFileNameForNonKDE4(icon_change_count, id));
215
216  int bytes_written =
217      base::WriteFile(new_file_path,
218                      png_data->front_as<char>(), png_data->size());
219
220  if (bytes_written != static_cast<int>(png_data->size()))
221    return base::FilePath();
222  return new_file_path;
223}
224
225void DeleteTempDirectory(const base::FilePath& dir_path) {
226  if (dir_path.empty())
227    return;
228  base::DeleteFile(dir_path, true);
229}
230
231}  // namespace
232
233namespace libgtk2ui {
234
235AppIndicatorIcon::AppIndicatorIcon(std::string id,
236                                   const gfx::ImageSkia& image,
237                                   const base::string16& tool_tip)
238    : id_(id),
239      using_kde4_(false),
240      icon_(NULL),
241      menu_model_(NULL),
242      icon_change_count_(0),
243      weak_factory_(this) {
244  scoped_ptr<base::Environment> env(base::Environment::Create());
245  using_kde4_ = base::nix::GetDesktopEnvironment(env.get()) ==
246      base::nix::DESKTOP_ENVIRONMENT_KDE4;
247
248  EnsureMethodsLoaded();
249  tool_tip_ = base::UTF16ToUTF8(tool_tip);
250  SetImage(image);
251}
252AppIndicatorIcon::~AppIndicatorIcon() {
253  if (icon_) {
254    app_indicator_set_status(icon_, APP_INDICATOR_STATUS_PASSIVE);
255    g_object_unref(icon_);
256    content::BrowserThread::GetBlockingPool()->PostTask(
257        FROM_HERE,
258        base::Bind(&DeleteTempDirectory, icon_file_path_.DirName()));
259  }
260}
261
262// static
263bool AppIndicatorIcon::CouldOpen() {
264  EnsureMethodsLoaded();
265  return g_opened;
266}
267
268void AppIndicatorIcon::SetImage(const gfx::ImageSkia& image) {
269  if (!g_opened)
270    return;
271
272  ++icon_change_count_;
273
274  // We create a deep copy of the image since it may have been freed by the time
275  // it's accessed in the other thread.
276  scoped_ptr<gfx::ImageSkia> safe_image(image.DeepCopy());
277  base::PostTaskAndReplyWithResult(
278      content::BrowserThread::GetBlockingPool()
279          ->GetTaskRunnerWithShutdownBehavior(
280                base::SequencedWorkerPool::SKIP_ON_SHUTDOWN).get(),
281      FROM_HERE,
282      base::Bind(&CreateTempImageFile,
283                 using_kde4_,
284                 safe_image.release(),
285                 icon_change_count_,
286                 id_,
287                 icon_file_path_),
288      base::Bind(&AppIndicatorIcon::SetImageFromFile,
289                 weak_factory_.GetWeakPtr()));
290}
291
292void AppIndicatorIcon::SetToolTip(const base::string16& tool_tip) {
293  DCHECK(!tool_tip_.empty());
294  tool_tip_ = base::UTF16ToUTF8(tool_tip);
295  UpdateClickActionReplacementMenuItem();
296}
297
298void AppIndicatorIcon::UpdatePlatformContextMenu(ui::MenuModel* model) {
299  if (!g_opened)
300    return;
301
302  menu_model_ = model;
303
304  // The icon is created asynchronously so it might not exist when the menu is
305  // set.
306  if (icon_)
307    SetMenu();
308}
309
310void AppIndicatorIcon::RefreshPlatformContextMenu() {
311  menu_->Refresh();
312}
313
314void AppIndicatorIcon::SetImageFromFile(const base::FilePath& icon_file_path) {
315  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
316  if (icon_file_path.empty())
317    return;
318
319  base::FilePath old_path = icon_file_path_;
320  icon_file_path_ = icon_file_path;
321
322  std::string icon_name =
323      icon_file_path_.BaseName().RemoveExtension().value();
324  std::string icon_dir = GetIconThemePath(using_kde4_, icon_file_path);
325  if (!icon_) {
326    icon_ =
327        app_indicator_new_with_path(id_.c_str(),
328                                    icon_name.c_str(),
329                                    APP_INDICATOR_CATEGORY_APPLICATION_STATUS,
330                                    icon_dir.c_str());
331    app_indicator_set_status(icon_, APP_INDICATOR_STATUS_ACTIVE);
332    SetMenu();
333  } else {
334    // Currently we are creating a new temp directory every time the icon is
335    // set. So we need to set the directory each time.
336    app_indicator_set_icon_theme_path(icon_, icon_dir.c_str());
337    app_indicator_set_icon_full(icon_, icon_name.c_str(), "icon");
338
339    if (ShouldCreateTempDirectoryPerImage(using_kde4_)) {
340      // Delete previous icon directory.
341      content::BrowserThread::GetBlockingPool()->PostTask(
342          FROM_HERE,
343          base::Bind(&DeleteTempDirectory, old_path.DirName()));
344    }
345  }
346}
347
348void AppIndicatorIcon::SetMenu() {
349  menu_.reset(new AppIndicatorIconMenu(menu_model_));
350  UpdateClickActionReplacementMenuItem();
351  app_indicator_set_menu(icon_, menu_->GetGtkMenu());
352}
353
354void AppIndicatorIcon::UpdateClickActionReplacementMenuItem() {
355  // The menu may not have been created yet.
356  if (!menu_.get())
357    return;
358
359  if (!delegate()->HasClickAction() && menu_model_)
360    return;
361
362  DCHECK(!tool_tip_.empty());
363  menu_->UpdateClickActionReplacementMenuItem(
364      tool_tip_.c_str(),
365      base::Bind(&AppIndicatorIcon::OnClickActionReplacementMenuItemActivated,
366                 base::Unretained(this)));
367}
368
369void AppIndicatorIcon::OnClickActionReplacementMenuItemActivated() {
370  if (delegate())
371    delegate()->OnClick();
372}
373
374}  // namespace libgtk2ui
375