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