Give platform apps control over launcher right-click context menu.

This moves most of the extension context menu related code from RenderViewContextMenu
to a separate class, which is then used in two other places as well.

This time without a bug that caused the 'tab' parameter in the onClicked event to
no longer get valid values (as well as an extension to a test so this problem is
not just caught in debug builds but also in tests run in release builds).
See http://codereview.chromium.org/10918103/ for the previous go at this patch.

BUG=143222

Review URL: https://chromiumcodereview.appspot.com/10979036

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@159312 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/api/context_menu/context_menu_api.cc b/chrome/browser/extensions/api/context_menu/context_menu_api.cc
index 102d5ee..39e3da4 100644
--- a/chrome/browser/extensions/api/context_menu/context_menu_api.cc
+++ b/chrome/browser/extensions/api/context_menu/context_menu_api.cc
@@ -34,6 +34,8 @@
     "Parent items must have type \"normal\"";
 const char kTitleNeededError[] =
     "All menu items except for separators must have a title";
+const char kLauncherNotAllowedError[] =
+    "Only packaged apps are allowed to use 'launcher' context";
 
 std::string GetIDString(const extensions::MenuItem::Id& id) {
   if (id.uid == 0)
@@ -75,6 +77,9 @@
       case PropertyWithEnumT::CONTEXTS_ELEMENT_FRAME:
         contexts.Add(extensions::MenuItem::FRAME);
         break;
+      case PropertyWithEnumT::CONTEXTS_ELEMENT_LAUNCHER:
+        contexts.Add(extensions::MenuItem::LAUNCHER);
+        break;
       case PropertyWithEnumT::CONTEXTS_ELEMENT_NONE:
         NOTREACHED();
     }
@@ -187,6 +192,12 @@
   else
     contexts.Add(MenuItem::PAGE);
 
+  if (contexts.Contains(MenuItem::LAUNCHER) &&
+      !GetExtension()->is_platform_app()) {
+    error_ = kLauncherNotAllowedError;
+    return false;
+  }
+
   MenuItem::Type type = GetType(params->create_properties);
 
   if (title.empty() && type != MenuItem::SEPARATOR) {
@@ -303,6 +314,13 @@
   MenuItem::ContextList contexts;
   if (params->update_properties.contexts.get()) {
     contexts = GetContexts(params->update_properties);
+
+    if (contexts.Contains(MenuItem::LAUNCHER) &&
+        !GetExtension()->is_platform_app()) {
+      error_ = kLauncherNotAllowedError;
+      return false;
+    }
+
     if (contexts != item->contexts())
       item->set_contexts(contexts);
   }
diff --git a/chrome/browser/extensions/context_menu_matcher.cc b/chrome/browser/extensions/context_menu_matcher.cc
new file mode 100644
index 0000000..67ca6d0
--- /dev/null
+++ b/chrome/browser/extensions/context_menu_matcher.cc
@@ -0,0 +1,232 @@
+// 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.
+
+#include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/extensions/context_menu_matcher.h"
+#include "chrome/browser/extensions/extension_service.h"
+#include "chrome/browser/profiles/profile.h"
+#include "content/public/common/context_menu_params.h"
+#include "ui/gfx/favicon_size.h"
+
+namespace extensions {
+
+// static
+const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
+
+ContextMenuMatcher::ContextMenuMatcher(
+    Profile* profile,
+    ui::SimpleMenuModel::Delegate* delegate,
+    ui::SimpleMenuModel* menu_model,
+    const base::Callback<bool(const MenuItem*)>& filter)
+    : profile_(profile), menu_model_(menu_model), delegate_(delegate),
+      filter_(filter) {
+}
+
+void ContextMenuMatcher::AppendExtensionItems(const std::string& extension_id,
+                                              const string16& selection_text,
+                                              int* index)
+{
+  ExtensionService* service = profile_->GetExtensionService();
+  MenuManager* manager = service->menu_manager();
+  const Extension* extension = service->GetExtensionById(extension_id, false);
+  DCHECK_GE(*index, 0);
+  int max_index =
+      IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST - IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
+  if (!extension || *index >= max_index)
+    return;
+
+  // Find matching items.
+  const MenuItem::List* all_items = manager->MenuItems(extension_id);
+  if (!all_items || all_items->empty())
+    return;
+  bool can_cross_incognito = service->CanCrossIncognito(extension);
+  MenuItem::List items = GetRelevantExtensionItems(*all_items,
+                                                   can_cross_incognito);
+
+  if (items.empty())
+    return;
+
+  // If this is the first extension-provided menu item, and there are other
+  // items in the menu, and the last item is not a separator add a separator.
+  if (*index == 0 && menu_model_->GetItemCount() &&
+      menu_model_->GetTypeAt(menu_model_->GetItemCount() - 1) !=
+          ui::MenuModel::TYPE_SEPARATOR)
+    menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
+
+  // Extensions (other than platform apps) are only allowed one top-level slot
+  // (and it can't be a radio or checkbox item because we are going to put the
+  // extension icon next to it).
+  // If they have more than that, we automatically push them into a submenu.
+  if (extension->is_platform_app()) {
+    RecursivelyAppendExtensionItems(items, can_cross_incognito, selection_text,
+                                    menu_model_, index);
+  } else {
+    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
+    string16 title;
+    MenuItem::List submenu_items;
+
+    if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
+      title = UTF8ToUTF16(extension->name());
+      submenu_items = items;
+    } else {
+      MenuItem* item = items[0];
+      extension_item_map_[menu_id] = item->id();
+      title = item->TitleWithReplacement(selection_text,
+                                       kMaxExtensionItemTitleLength);
+      submenu_items = GetRelevantExtensionItems(item->children(),
+                                                can_cross_incognito);
+    }
+
+    // Now add our item(s) to the menu_model_.
+    if (submenu_items.empty()) {
+      menu_model_->AddItem(menu_id, title);
+    } else {
+      ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
+      extension_menu_models_.push_back(submenu);
+      menu_model_->AddSubMenu(menu_id, title, submenu);
+      RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
+                                      selection_text, submenu, index);
+    }
+    SetExtensionIcon(extension_id);
+  }
+}
+
+void ContextMenuMatcher::Clear() {
+  extension_item_map_.clear();
+  extension_menu_models_.clear();
+}
+
+bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
+  MenuItem* item = GetExtensionMenuItem(command_id);
+  if (!item)
+    return false;
+  return item->checked();
+}
+
+bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
+  MenuItem* item = GetExtensionMenuItem(command_id);
+  if (!item)
+    return true;
+  return item->enabled();
+}
+
+void ContextMenuMatcher::ExecuteCommand(int command_id,
+    content::WebContents* web_contents,
+    const content::ContextMenuParams& params) {
+  MenuManager* manager = profile_->GetExtensionService()->menu_manager();
+  MenuItem* item = GetExtensionMenuItem(command_id);
+  if (!item)
+    return;
+
+  manager->ExecuteCommand(profile_, web_contents, params, item->id());
+}
+
+MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
+    const MenuItem::List& items,
+    bool can_cross_incognito) {
+  MenuItem::List result;
+  for (MenuItem::List::const_iterator i = items.begin();
+       i != items.end(); ++i) {
+    const MenuItem* item = *i;
+
+    if (!filter_.Run(item))
+      continue;
+
+    if (item->id().incognito == profile_->IsOffTheRecord() ||
+        can_cross_incognito)
+      result.push_back(*i);
+  }
+  return result;
+}
+
+void ContextMenuMatcher::RecursivelyAppendExtensionItems(
+    const MenuItem::List& items,
+    bool can_cross_incognito,
+    const string16& selection_text,
+    ui::SimpleMenuModel* menu_model,
+    int* index)
+{
+  MenuItem::Type last_type = MenuItem::NORMAL;
+  int radio_group_id = 1;
+
+  for (MenuItem::List::const_iterator i = items.begin();
+       i != items.end(); ++i) {
+    MenuItem* item = *i;
+
+    // If last item was of type radio but the current one isn't, auto-insert
+    // a separator.  The converse case is handled below.
+    if (last_type == MenuItem::RADIO &&
+        item->type() != MenuItem::RADIO) {
+      menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
+      last_type = MenuItem::SEPARATOR;
+    }
+
+    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
+    if (menu_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST)
+      return;
+    extension_item_map_[menu_id] = item->id();
+    string16 title = item->TitleWithReplacement(selection_text,
+                                                kMaxExtensionItemTitleLength);
+    if (item->type() == MenuItem::NORMAL) {
+      MenuItem::List children =
+          GetRelevantExtensionItems(item->children(), can_cross_incognito);
+      if (children.empty()) {
+        menu_model->AddItem(menu_id, title);
+      } else {
+        ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
+        extension_menu_models_.push_back(submenu);
+        menu_model->AddSubMenu(menu_id, title, submenu);
+        RecursivelyAppendExtensionItems(children, can_cross_incognito,
+                                        selection_text, submenu, index);
+      }
+    } else if (item->type() == MenuItem::CHECKBOX) {
+      menu_model->AddCheckItem(menu_id, title);
+    } else if (item->type() == MenuItem::RADIO) {
+      if (i != items.begin() &&
+          last_type != MenuItem::RADIO) {
+        radio_group_id++;
+
+        // Auto-append a separator if needed.
+        if (last_type != MenuItem::SEPARATOR)
+          menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
+      }
+
+      menu_model->AddRadioItem(menu_id, title, radio_group_id);
+    } else if (item->type() == MenuItem::SEPARATOR) {
+      if (i != items.begin() && last_type != MenuItem::SEPARATOR) {
+        menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
+      }
+    }
+    last_type = item->type();
+  }
+}
+
+MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
+  MenuManager* manager = profile_->GetExtensionService()->menu_manager();
+  std::map<int, MenuItem::Id>::const_iterator i =
+      extension_item_map_.find(id);
+  if (i != extension_item_map_.end()) {
+    MenuItem* item = manager->GetItemById(i->second);
+    if (item)
+      return item;
+  }
+  return NULL;
+}
+
+void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
+  ExtensionService* service = profile_->GetExtensionService();
+  MenuManager* menu_manager = service->menu_manager();
+
+  int index = menu_model_->GetItemCount() - 1;
+  DCHECK_GE(index, 0);
+
+  const SkBitmap& icon = menu_manager->GetIconForExtension(extension_id);
+  DCHECK(icon.width() == gfx::kFaviconSize);
+  DCHECK(icon.height() == gfx::kFaviconSize);
+
+  menu_model_->SetIcon(index, gfx::Image(icon));
+}
+
+}  // namespace extensions
diff --git a/chrome/browser/extensions/context_menu_matcher.h b/chrome/browser/extensions/context_menu_matcher.h
new file mode 100644
index 0000000..3dee9bd
--- /dev/null
+++ b/chrome/browser/extensions/context_menu_matcher.h
@@ -0,0 +1,86 @@
+// 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.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_MATCHER_H_
+#define CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_MATCHER_H_
+
+#include <map>
+
+#include "base/callback.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "chrome/browser/extensions/menu_manager.h"
+#include "ui/base/models/simple_menu_model.h"
+
+class ExtensionContextMenuBrowserTest;
+class Profile;
+
+namespace extensions {
+
+// This class contains code that is shared between the various places where
+// context menu items added by the extension or app should be shown.
+class ContextMenuMatcher {
+ public:
+  static const size_t kMaxExtensionItemTitleLength;
+
+  // The |filter| will be called on possibly matching menu items, and its
+  // result is used to determine which items to actually append to the menu.
+  ContextMenuMatcher(Profile* profile,
+                     ui::SimpleMenuModel::Delegate* delegate,
+                     ui::SimpleMenuModel* menu_model,
+                     const base::Callback<bool(const MenuItem*)>& filter);
+
+  // This is a helper function to append items for one particular extension.
+  // The |index| parameter is used for assigning id's, and is incremented for
+  // each item actually added.
+  void AppendExtensionItems(const std::string& extension_id,
+                            const string16& selection_text,
+                            int* index);
+
+  void Clear();
+
+  bool IsCommandIdChecked(int command_id) const;
+  bool IsCommandIdEnabled(int command_id) const;
+  void ExecuteCommand(int command_id,
+                      content::WebContents* web_contents,
+                      const content::ContextMenuParams& params);
+
+ private:
+  friend class ::ExtensionContextMenuBrowserTest;
+
+  MenuItem::List GetRelevantExtensionItems(
+      const MenuItem::List& items,
+      bool can_cross_incognito);
+
+  // Used for recursively adding submenus of extension items.
+  void RecursivelyAppendExtensionItems(const MenuItem::List& items,
+                                       bool can_cross_incognito,
+                                       const string16& selection_text,
+                                       ui::SimpleMenuModel* menu_model,
+                                       int* index);
+
+  // Attempts to get an MenuItem given the id of a context menu item.
+  extensions::MenuItem* GetExtensionMenuItem(int id) const;
+
+  // This will set the icon on the most recently-added item in the menu_model_.
+  void SetExtensionIcon(const std::string& extension_id);
+
+  Profile* profile_;
+  ui::SimpleMenuModel* menu_model_;
+  ui::SimpleMenuModel::Delegate* delegate_;
+
+  base::Callback<bool(const MenuItem*)> filter_;
+
+  // Maps the id from a context menu item to the MenuItem's internal id.
+  std::map<int, extensions::MenuItem::Id> extension_item_map_;
+
+  // Keep track of and clean up menu models for submenus.
+  ScopedVector<ui::SimpleMenuModel> extension_menu_models_;
+
+  DISALLOW_COPY_AND_ASSIGN(ContextMenuMatcher);
+};
+
+}  // namespace extensions
+
+#endif  // CHROME_BROWSER_EXTENSIONS_CONTEXT_MENU_MATCHER_H_
diff --git a/chrome/browser/extensions/extension_context_menu_browsertest.cc b/chrome/browser/extensions/extension_context_menu_browsertest.cc
index 442d2ab..10bf72e 100644
--- a/chrome/browser/extensions/extension_context_menu_browsertest.cc
+++ b/chrome/browser/extensions/extension_context_menu_browsertest.cc
@@ -39,36 +39,6 @@
 
   virtual ~TestRenderViewContextMenu() {}
 
-  bool HasExtensionItemWithLabel(const std::string& label) {
-    string16 label16 = UTF8ToUTF16(label);
-    std::map<int, MenuItem::Id>::iterator i;
-    for (i = extension_item_map_.begin(); i != extension_item_map_.end(); ++i) {
-      const MenuItem::Id& id = i->second;
-      string16 tmp_label;
-      EXPECT_TRUE(GetItemLabel(id, &tmp_label));
-      if (tmp_label == label16)
-        return true;
-    }
-    return false;
-  }
-
-  // Looks in the menu for an extension item with |id|, and if it is found and
-  // has a label, that is put in |result| and we return true. Otherwise returns
-  // false.
-  bool GetItemLabel(const MenuItem::Id& id, string16* result) {
-    int command_id = 0;
-    if (!FindCommandId(id, &command_id))
-      return false;
-
-    MenuModel* model = NULL;
-    int index = -1;
-    if (!GetMenuModelAndItemIndex(command_id, &model, &index)) {
-      return false;
-    }
-    *result = model->GetLabelAt(index);
-    return true;
-  }
-
   // Searches for an menu item with |command_id|. If it's found, the return
   // value is true and the model and index where it appears in that model are
   // returned in |found_model| and |found_index|. Otherwise returns false.
@@ -95,17 +65,8 @@
     return false;
   }
 
-  // Given an extension menu item id, tries to find the corresponding command id
-  // in the menu.
-  bool FindCommandId(const MenuItem::Id& id, int* command_id) {
-    std::map<int, MenuItem::Id>::const_iterator i;
-    for (i = extension_item_map_.begin(); i != extension_item_map_.end(); ++i) {
-      if (i->second == id) {
-        *command_id = i->first;
-        return true;
-      }
-    }
-    return false;
+  extensions::ContextMenuMatcher& extension_items() {
+    return extension_items_;
   }
 
  protected:
@@ -197,7 +158,7 @@
                             const std::string& label) {
     scoped_ptr<TestRenderViewContextMenu> menu(
         CreateMenu(browser(), page_url, link_url, frame_url));
-    return menu->HasExtensionItemWithLabel(label);
+    return MenuHasExtensionItemWithLabel(menu.get(), label);
   }
 
   // This creates an extension that starts |enabled| and then switches to
@@ -233,6 +194,56 @@
     ASSERT_TRUE(update.WaitUntilSatisfied());
     ASSERT_EQ(!enabled, menu->IsCommandIdEnabled(command_id));
   }
+
+ bool MenuHasExtensionItemWithLabel(TestRenderViewContextMenu* menu,
+                                     const std::string& label) {
+    string16 label16 = UTF8ToUTF16(label);
+    std::map<int, MenuItem::Id>::iterator i;
+    for (i = menu->extension_items().extension_item_map_.begin();
+         i != menu->extension_items().extension_item_map_.end(); ++i) {
+      const MenuItem::Id& id = i->second;
+      string16 tmp_label;
+      EXPECT_TRUE(GetItemLabel(menu, id, &tmp_label));
+      if (tmp_label == label16)
+        return true;
+    }
+    return false;
+  }
+
+  // Looks in the menu for an extension item with |id|, and if it is found and
+  // has a label, that is put in |result| and we return true. Otherwise returns
+  // false.
+  bool GetItemLabel(TestRenderViewContextMenu* menu,
+                    const MenuItem::Id& id,
+                    string16* result) {
+    int command_id = 0;
+    if (!FindCommandId(menu, id, &command_id))
+      return false;
+
+    MenuModel* model = NULL;
+    int index = -1;
+    if (!menu->GetMenuModelAndItemIndex(command_id, &model, &index)) {
+      return false;
+    }
+    *result = model->GetLabelAt(index);
+    return true;
+  }
+
+  // Given an extension menu item id, tries to find the corresponding command id
+  // in the menu.
+  bool FindCommandId(TestRenderViewContextMenu* menu,
+                     const MenuItem::Id& id,
+                     int* command_id) {
+    std::map<int, MenuItem::Id>::const_iterator i;
+    for (i = menu->extension_items().extension_item_map_.begin();
+         i != menu->extension_items().extension_item_map_.end(); ++i) {
+      if (i->second == id) {
+        *command_id = i->first;
+        return true;
+      }
+    }
+    return false;
+  }
 };
 
 // Tests adding a simple context menu item.
@@ -302,7 +313,7 @@
   ASSERT_TRUE(listener.WaitUntilSatisfied());
 
   // Make sure we have an item registered with a long title.
-  size_t limit = RenderViewContextMenu::kMaxExtensionItemTitleLength;
+  size_t limit = extensions::ContextMenuMatcher::kMaxExtensionItemTitleLength;
   MenuItem::List items = GetItems();
   ASSERT_EQ(1u, items.size());
   MenuItem* item = items.at(0);
@@ -315,7 +326,7 @@
       CreateMenu(browser(), url, GURL(), GURL()));
 
   string16 label;
-  ASSERT_TRUE(menu->GetItemLabel(item->id(), &label));
+  ASSERT_TRUE(GetItemLabel(menu.get(), item->id(), &label));
   ASSERT_TRUE(label.size() <= limit);
 }
 
@@ -580,7 +591,7 @@
   MenuItem::Id id(false, extension->id());
   id.string_uid = "checkbox1";
   int command_id = -1;
-  ASSERT_TRUE(menu->FindCommandId(id, &command_id));
+  ASSERT_TRUE(FindCommandId(menu.get(), id, &command_id));
   EXPECT_FALSE(menu->IsCommandIdChecked(command_id));
 
   // Executing the checkbox also fires the onClicked event.
diff --git a/chrome/browser/extensions/menu_manager.h b/chrome/browser/extensions/menu_manager.h
index 9ffe0fcf..035a16d 100644
--- a/chrome/browser/extensions/menu_manager.h
+++ b/chrome/browser/extensions/menu_manager.h
@@ -71,6 +71,7 @@
     VIDEO = 64,
     AUDIO = 128,
     FRAME = 256,
+    LAUNCHER = 512
   };
 
   // An item can be only one of these types.
diff --git a/chrome/browser/tab_contents/render_view_context_menu.cc b/chrome/browser/tab_contents/render_view_context_menu.cc
index 2c3bdae..e716be576 100644
--- a/chrome/browser/tab_contents/render_view_context_menu.cc
+++ b/chrome/browser/tab_contents/render_view_context_menu.cc
@@ -227,8 +227,6 @@
 }  // namespace
 
 // static
-const size_t RenderViewContextMenu::kMaxExtensionItemTitleLength = 75;
-// static
 const size_t RenderViewContextMenu::kMaxSelectionTextLength = 50;
 
 // static
@@ -253,6 +251,8 @@
       source_web_contents_(web_contents),
       profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())),
       ALLOW_THIS_IN_INITIALIZER_LIST(menu_model_(this)),
+      extension_items_(profile_, this, &menu_model_,
+                    base::Bind(MenuItemMatchesParams, params_)),
       external_(false),
       ALLOW_THIS_IN_INITIALIZER_LIST(speech_input_submenu_model_(this)),
       ALLOW_THIS_IN_INITIALIZER_LIST(protocol_handler_submenu_model_(this)),
@@ -338,177 +338,21 @@
   return params.frame_url.is_empty() ? params.page_url : params.frame_url;
 }
 
-// Given a list of items, returns the ones that match given the contents
-// of |params| and the profile.
 // static
-MenuItem::List RenderViewContextMenu::GetRelevantExtensionItems(
-    const MenuItem::List& items,
+bool RenderViewContextMenu::MenuItemMatchesParams(
     const content::ContextMenuParams& params,
-    Profile* profile,
-    bool can_cross_incognito) {
-  MenuItem::List result;
-  for (MenuItem::List::const_iterator i = items.begin();
-       i != items.end(); ++i) {
-    const MenuItem* item = *i;
+    const extensions::MenuItem* item) {
+  bool match = ExtensionContextAndPatternMatch(params, item->contexts(),
+                                               item->target_url_patterns());
+  if (!match)
+    return false;
 
-    if (!ExtensionContextAndPatternMatch(params, item->contexts(),
-        item->target_url_patterns()))
-      continue;
-
-    const GURL& document_url = GetDocumentURL(params);
-    if (!ExtensionPatternMatch(item->document_url_patterns(), document_url))
-      continue;
-
-    if (item->id().incognito == profile->IsOffTheRecord() ||
-        can_cross_incognito)
-      result.push_back(*i);
-  }
-  return result;
-}
-
-void RenderViewContextMenu::AppendExtensionItems(
-    const std::string& extension_id, int* index) {
-  ExtensionService* service = profile_->GetExtensionService();
-  MenuManager* manager = service->menu_manager();
-  const Extension* extension = service->GetExtensionById(extension_id, false);
-  DCHECK_GE(*index, 0);
-  int max_index =
-      IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST - IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
-  if (!extension || *index >= max_index)
-    return;
-
-  // Find matching items.
-  const MenuItem::List* all_items = manager->MenuItems(extension_id);
-  if (!all_items || all_items->empty())
-    return;
-  bool can_cross_incognito = service->CanCrossIncognito(extension);
-  MenuItem::List items =
-      GetRelevantExtensionItems(*all_items, params_, profile_,
-                                can_cross_incognito);
-  if (items.empty())
-    return;
-
-  // If this is the first extension-provided menu item, and there are other
-  // items in the menu, add a separator.
-  if (*index == 0 && menu_model_.GetItemCount())
-    menu_model_.AddSeparator(ui::NORMAL_SEPARATOR);
-
-  // Extensions (other than platform apps) are only allowed one top-level slot
-  // (and it can't be a radio or checkbox item because we are going to put the
-  // extension icon next to it).
-  // If they have more than that, we automatically push them into a submenu.
-  if (extension->is_platform_app()) {
-    RecursivelyAppendExtensionItems(items, can_cross_incognito, &menu_model_,
-                                    index);
-  } else {
-    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
-    string16 title;
-    MenuItem::List submenu_items;
-
-    if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
-      title = UTF8ToUTF16(extension->name());
-      submenu_items = items;
-    } else {
-      MenuItem* item = items[0];
-      extension_item_map_[menu_id] = item->id();
-      title = item->TitleWithReplacement(PrintableSelectionText(),
-                                       kMaxExtensionItemTitleLength);
-      submenu_items = GetRelevantExtensionItems(item->children(), params_,
-                                              profile_, can_cross_incognito);
-    }
-
-    // Now add our item(s) to the menu_model_.
-    if (submenu_items.empty()) {
-      menu_model_.AddItem(menu_id, title);
-    } else {
-      ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(this);
-      extension_menu_models_.push_back(submenu);
-      menu_model_.AddSubMenu(menu_id, title, submenu);
-      RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
-                                      submenu, index);
-    }
-    SetExtensionIcon(extension_id);
-  }
-}
-
-void RenderViewContextMenu::RecursivelyAppendExtensionItems(
-    const MenuItem::List& items,
-    bool can_cross_incognito,
-    ui::SimpleMenuModel* menu_model,
-    int* index) {
-  string16 selection_text = PrintableSelectionText();
-  MenuItem::Type last_type = MenuItem::NORMAL;
-  int radio_group_id = 1;
-
-  for (MenuItem::List::const_iterator i = items.begin();
-       i != items.end(); ++i) {
-    MenuItem* item = *i;
-
-    // If last item was of type radio but the current one isn't, auto-insert
-    // a separator.  The converse case is handled below.
-    if (last_type == MenuItem::RADIO &&
-        item->type() != MenuItem::RADIO) {
-      menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
-      last_type = MenuItem::SEPARATOR;
-    }
-
-    int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++;
-    if (menu_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST)
-      return;
-    extension_item_map_[menu_id] = item->id();
-    string16 title = item->TitleWithReplacement(selection_text,
-                                                kMaxExtensionItemTitleLength);
-    if (item->type() == MenuItem::NORMAL) {
-      MenuItem::List children =
-          GetRelevantExtensionItems(item->children(), params_,
-                                    profile_, can_cross_incognito);
-      if (children.empty()) {
-        menu_model->AddItem(menu_id, title);
-      } else {
-        ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(this);
-        extension_menu_models_.push_back(submenu);
-        menu_model->AddSubMenu(menu_id, title, submenu);
-        RecursivelyAppendExtensionItems(children, can_cross_incognito,
-                                        submenu, index);
-      }
-    } else if (item->type() == MenuItem::CHECKBOX) {
-      menu_model->AddCheckItem(menu_id, title);
-    } else if (item->type() == MenuItem::RADIO) {
-      if (i != items.begin() &&
-          last_type != MenuItem::RADIO) {
-        radio_group_id++;
-
-        // Auto-append a separator if needed.
-        if (last_type != MenuItem::SEPARATOR)
-          menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
-      }
-
-      menu_model->AddRadioItem(menu_id, title, radio_group_id);
-    } else if (item->type() == MenuItem::SEPARATOR) {
-      if (i != items.begin() && last_type != MenuItem::SEPARATOR) {
-        menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
-      }
-    }
-    last_type = item->type();
-  }
-}
-
-void RenderViewContextMenu::SetExtensionIcon(const std::string& extension_id) {
-  ExtensionService* service = profile_->GetExtensionService();
-  MenuManager* menu_manager = service->menu_manager();
-
-  int index = menu_model_.GetItemCount() - 1;
-  DCHECK_GE(index, 0);
-
-  const SkBitmap& icon = menu_manager->GetIconForExtension(extension_id);
-  DCHECK(icon.width() == gfx::kFaviconSize);
-  DCHECK(icon.height() == gfx::kFaviconSize);
-
-  menu_model_.SetIcon(index, gfx::Image(icon));
+  const GURL& document_url = GetDocumentURL(params);
+  return ExtensionPatternMatch(item->document_url_patterns(), document_url);
 }
 
 void RenderViewContextMenu::AppendAllExtensionItems() {
-  extension_item_map_.clear();
+  extension_items_.Clear();
   ExtensionService* service = profile_->GetExtensionService();
   if (!service)
     return;  // In unit-tests, we may not have an ExtensionService.
@@ -537,7 +381,8 @@
   std::vector<std::pair<std::string, std::string> >::const_iterator i;
   for (i = sorted_ids.begin();
        i != sorted_ids.end(); ++i) {
-    AppendExtensionItems(i->second, &index);
+    extension_items_.AppendExtensionItems(i->second, PrintableSelectionText(),
+                                       &index);
   }
   UMA_HISTOGRAM_TIMES("Extensions.ContextMenus_BuildTime",
                       base::TimeTicks::Now() - begin);
@@ -668,7 +513,8 @@
     AppendCopyItem();
 
   int index = 0;
-  AppendExtensionItems(platform_app->id(), &index);
+  extension_items_.AppendExtensionItems(platform_app->id(),
+                                     PrintableSelectionText(), &index);
 
   // Add dev tools for unpacked extensions.
   if (platform_app->location() == Extension::LOAD) {
@@ -1056,18 +902,6 @@
 void RenderViewContextMenu::AppendPlatformEditableItems() {
 }
 
-MenuItem* RenderViewContextMenu::GetExtensionMenuItem(int id) const {
-  MenuManager* manager = profile_->GetExtensionService()->menu_manager();
-  std::map<int, MenuItem::Id>::const_iterator i =
-      extension_item_map_.find(id);
-  if (i != extension_item_map_.end()) {
-    MenuItem* item = manager->GetItemById(i->second);
-    if (item)
-      return item;
-  }
-  return NULL;
-}
-
 // Menu delegate functions -----------------------------------------------------
 
 bool RenderViewContextMenu::IsCommandIdEnabled(int id) const {
@@ -1107,11 +941,7 @@
   // Extension items.
   if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
       id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
-    MenuItem* item = GetExtensionMenuItem(id);
-    // If this is the parent menu item, it is always enabled.
-    if (!item)
-      return true;
-    return item->enabled();
+    return extension_items_.IsCommandIdEnabled(id);
   }
 
   if (id >= IDC_CONTENT_CONTEXT_PROTOCOL_HANDLER_FIRST &&
@@ -1408,11 +1238,7 @@
   // Extension items.
   if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
       id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
-    MenuItem* item = GetExtensionMenuItem(id);
-    if (item)
-      return item->checked();
-    else
-      return false;
+    return extension_items_.IsCommandIdChecked(id);
   }
 
 #if defined(ENABLE_INPUT_SPEECH)
@@ -1453,13 +1279,7 @@
   // Process extension menu items.
   if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
       id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
-    MenuManager* manager = profile_->GetExtensionService()->menu_manager();
-    std::map<int, MenuItem::Id>::const_iterator i =
-        extension_item_map_.find(id);
-    if (i != extension_item_map_.end()) {
-      manager->ExecuteCommand(profile_, source_web_contents_, params_,
-                              i->second);
-    }
+    extension_items_.ExecuteCommand(id, source_web_contents_, params_);
     return;
   }
 
diff --git a/chrome/browser/tab_contents/render_view_context_menu.h b/chrome/browser/tab_contents/render_view_context_menu.h
index fc6a80b..b50b292 100644
--- a/chrome/browser/tab_contents/render_view_context_menu.h
+++ b/chrome/browser/tab_contents/render_view_context_menu.h
@@ -14,6 +14,7 @@
 #include "base/observer_list.h"
 #include "base/string16.h"
 #include "chrome/browser/custom_handlers/protocol_handler_registry.h"
+#include "chrome/browser/extensions/context_menu_matcher.h"
 #include "chrome/browser/extensions/menu_manager.h"
 #include "chrome/browser/tab_contents/render_view_context_menu_observer.h"
 #include "content/public/common/context_menu_params.h"
@@ -127,7 +128,6 @@
 class RenderViewContextMenu : public ui::SimpleMenuModel::Delegate,
                               public RenderViewContextMenuProxy {
  public:
-  static const size_t kMaxExtensionItemTitleLength;
   static const size_t kMaxSelectionTextLength;
 
   RenderViewContextMenu(content::WebContents* web_contents,
@@ -178,21 +178,16 @@
       ui::Accelerator* accelerator) = 0;
   virtual void AppendPlatformEditableItems();
 
-  // Attempts to get an MenuItem given the id of a context menu item.
-  extensions::MenuItem* GetExtensionMenuItem(int id) const;
-
   content::ContextMenuParams params_;
   content::WebContents* source_web_contents_;
   Profile* profile_;
 
   ui::SimpleMenuModel menu_model_;
+  extensions::ContextMenuMatcher extension_items_;
 
   // True if we are showing for an external tab contents. The default is false.
   bool external_;
 
-  // Maps the id from a context menu item to the MenuItem's internal id.
-  std::map<int, extensions::MenuItem::Id> extension_item_map_;
-
  private:
   friend class RenderViewContextMenuTest;
 
@@ -202,11 +197,9 @@
       const content::ContextMenuParams& params,
       extensions::MenuItem::ContextList contexts,
       const URLPatternSet& target_url_patterns);
-  static extensions::MenuItem::List GetRelevantExtensionItems(
-      const extensions::MenuItem::List& items,
+  static bool MenuItemMatchesParams(
       const content::ContextMenuParams& params,
-      Profile* profile,
-      bool can_cross_incognito);
+      const extensions::MenuItem* item);
 
   // Gets the extension (if any) associated with the WebContents that we're in.
   const extensions::Extension* GetExtension() const;
@@ -231,20 +224,6 @@
   void AppendSpeechInputOptionsSubMenu();
   void AppendProtocolHandlerSubMenu();
 
-  // This is a helper function to append items for one particular extension.
-  // The |index| parameter is used for assigning id's, and is incremented for
-  // each item actually added.
-  void AppendExtensionItems(const std::string& extension_id, int* index);
-
-  // Used for recursively adding submenus of extension items.
-  void RecursivelyAppendExtensionItems(
-      const std::vector<extensions::MenuItem*>& items,
-      bool can_cross_incognito,
-      ui::SimpleMenuModel* menu_model,
-      int* index);
-  // This will set the icon on the most recently-added item in the menu_model_.
-  void SetExtensionIcon(const std::string& extension_id);
-
   // Opens the specified URL string in a new tab.  The |frame_id| specifies the
   // frame in which the context menu was displayed, or 0 if the menu action is
   // independent of that frame (e.g. protocol handler settings).
diff --git a/chrome/browser/ui/app_list/extension_app_item.cc b/chrome/browser/ui/app_list/extension_app_item.cc
index 4d2ba75..871246b1 100644
--- a/chrome/browser/ui/app_list/extension_app_item.cc
+++ b/chrome/browser/ui/app_list/extension_app_item.cc
@@ -5,6 +5,8 @@
 #include "chrome/browser/ui/app_list/extension_app_item.h"
 
 #include "base/utf_string_conversions.h"
+#include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/extensions/context_menu_matcher.h"
 #include "chrome/browser/extensions/extension_prefs.h"
 #include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/extensions/extension_sorting.h"
@@ -20,6 +22,7 @@
 #include "chrome/common/extensions/extension.h"
 #include "chrome/common/extensions/extension_constants.h"
 #include "chrome/common/extensions/extension_icon_set.h"
+#include "content/public/common/context_menu_params.h"
 #include "grit/chromium_strings.h"
 #include "grit/generated_resources.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -121,6 +124,10 @@
   return profile->GetExtensionService()->extension_prefs()->extension_sorting();
 }
 
+bool MenuItemHasLauncherContext(const extensions::MenuItem* item) {
+  return item->contexts().Contains(extensions::MenuItem::LAUNCHER);
+}
+
 }  // namespace
 
 ExtensionAppItem::ExtensionAppItem(Profile* profile,
@@ -215,6 +222,9 @@
   if (command_id >= LAUNCH_TYPE_START && command_id < LAUNCH_TYPE_LAST) {
     return static_cast<int>(GetExtensionLaunchType(profile_, extension_id_)) +
         LAUNCH_TYPE_START == command_id;
+  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
+             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
+    return extension_menu_items_->IsCommandIdChecked(command_id);
   }
   return false;
 }
@@ -235,6 +245,9 @@
   } else if (command_id == DETAILS) {
     const Extension* extension = GetExtension();
     return extension && extension->from_webstore();
+  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
+             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
+    return extension_menu_items_->IsCommandIdEnabled(command_id);
   }
   return true;
 }
@@ -265,6 +278,10 @@
     StartExtensionUninstall();
   } else if (command_id == DETAILS) {
     ShowExtensionDetails();
+  } else if (command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
+             command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
+    extension_menu_items_->ExecuteCommand(command_id, NULL,
+                                          content::ContextMenuParams());
   }
 }
 
@@ -283,7 +300,14 @@
 
   if (!context_menu_model_.get()) {
     context_menu_model_.reset(new ui::SimpleMenuModel(this));
+    extension_menu_items_.reset(new extensions::ContextMenuMatcher(
+        profile_, this, context_menu_model_.get(),
+        base::Bind(MenuItemHasLauncherContext)));
+
     context_menu_model_->AddItem(LAUNCH, UTF8ToUTF16(title()));
+    int index = 0;
+    extension_menu_items_->AppendExtensionItems(extension_id_, string16(),
+                                                &index);
     context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
     context_menu_model_->AddItemWithStringId(
         TOGGLE_PIN,
diff --git a/chrome/browser/ui/app_list/extension_app_item.h b/chrome/browser/ui/app_list/extension_app_item.h
index de91826..88902bce 100644
--- a/chrome/browser/ui/app_list/extension_app_item.h
+++ b/chrome/browser/ui/app_list/extension_app_item.h
@@ -19,6 +19,7 @@
 class SkBitmap;
 
 namespace extensions {
+class ContextMenuMatcher;
 class Extension;
 }
 
@@ -73,6 +74,7 @@
 
   scoped_ptr<extensions::IconImage> icon_;
   scoped_ptr<ui::SimpleMenuModel> context_menu_model_;
+  scoped_ptr<extensions::ContextMenuMatcher> extension_menu_items_;
 
   DISALLOW_COPY_AND_ASSIGN(ExtensionAppItem);
 };
diff --git a/chrome/browser/ui/ash/launcher/chrome_launcher_controller.cc b/chrome/browser/ui/ash/launcher/chrome_launcher_controller.cc
index 4177861..39a928b 100644
--- a/chrome/browser/ui/ash/launcher/chrome_launcher_controller.cc
+++ b/chrome/browser/ui/ash/launcher/chrome_launcher_controller.cc
@@ -404,6 +404,13 @@
   return 0;
 }
 
+std::string ChromeLauncherController::GetAppIDForLauncherID(
+    ash::LauncherID id) {
+  DCHECK(id_to_item_controller_map_.find(id) !=
+      id_to_item_controller_map_.end());
+  return id_to_item_controller_map_[id]->app_id();
+}
+
 void ChromeLauncherController::SetAppImage(const std::string& id,
                                            const gfx::ImageSkia& image) {
   // TODO: need to get this working for shortcuts.
diff --git a/chrome/browser/ui/ash/launcher/chrome_launcher_controller.h b/chrome/browser/ui/ash/launcher/chrome_launcher_controller.h
index d022c2ef..027344c7 100644
--- a/chrome/browser/ui/ash/launcher/chrome_launcher_controller.h
+++ b/chrome/browser/ui/ash/launcher/chrome_launcher_controller.h
@@ -167,6 +167,7 @@
   std::string GetAppID(TabContents* tab);
 
   ash::LauncherID GetLauncherIDForAppID(const std::string& app_id);
+  std::string GetAppIDForLauncherID(ash::LauncherID id);
 
   // Sets the image for an app tab. This is intended to be invoked from the
   // AppIconLoader.
diff --git a/chrome/browser/ui/ash/launcher/launcher_context_menu.cc b/chrome/browser/ui/ash/launcher/launcher_context_menu.cc
index ca1b3ca0..ca4560d 100644
--- a/chrome/browser/ui/ash/launcher/launcher_context_menu.cc
+++ b/chrome/browser/ui/ash/launcher/launcher_context_menu.cc
@@ -7,18 +7,31 @@
 #include "ash/launcher/launcher_context_menu.h"
 #include "ash/shell.h"
 #include "base/command_line.h"
+#include "chrome/browser/extensions/context_menu_matcher.h"
 #include "chrome/browser/extensions/extension_prefs.h"
 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
 #include "chrome/common/chrome_switches.h"
+#include "content/public/common/context_menu_params.h"
 #include "grit/ash_strings.h"
 #include "grit/generated_resources.h"
 #include "ui/base/l10n/l10n_util.h"
 
+namespace {
+
+bool MenuItemHasLauncherContext(const extensions::MenuItem* item) {
+  return item->contexts().Contains(extensions::MenuItem::LAUNCHER);
+}
+
+}  // namespace
+
 LauncherContextMenu::LauncherContextMenu(ChromeLauncherController* controller,
                                          const ash::LauncherItem* item)
     : ui::SimpleMenuModel(NULL),
       controller_(controller),
-      item_(item ? *item : ash::LauncherItem()) {
+      item_(item ? *item : ash::LauncherItem()),
+      extension_items_(new extensions::ContextMenuMatcher(
+          controller->profile(), this, this,
+          base::Bind(MenuItemHasLauncherContext))) {
   set_delegate(this);
 
   if (is_valid_item()) {
@@ -67,6 +80,14 @@
     }
     AddSeparator(ui::NORMAL_SEPARATOR);
   }
+  std::string app_id = controller->GetAppIDForLauncherID(item_.id);
+  if (!app_id.empty()) {
+    int index = 0;
+    extension_items_->AppendExtensionItems(
+        app_id, string16(), &index);
+    if (index > 0)
+      AddSeparator(ui::NORMAL_SEPARATOR);
+  }
   AddCheckItemWithStringId(
       MENU_AUTO_HIDE, ash::LauncherContextMenu::GetAutoHideResourceStringId());
   if (CommandLine::ForCurrentProcess()->HasSwitch(
@@ -97,7 +118,7 @@
     case MENU_AUTO_HIDE:
       return ash::LauncherContextMenu::IsAutoHideMenuHideChecked();
     default:
-      return false;
+      return extension_items_->IsCommandIdChecked(command_id);
   }
 }
 
@@ -107,7 +128,7 @@
       return item_.type == ash::TYPE_PLATFORM_APP ||
           controller_->IsPinnable(item_.id);
     default:
-      return true;
+      return extension_items_->IsCommandIdEnabled(command_id);
   }
 }
 
@@ -156,5 +177,8 @@
       break;
     case MENU_ALIGNMENT_MENU:
       break;
+    default:
+      extension_items_->ExecuteCommand(command_id, NULL,
+                                       content::ContextMenuParams());
   }
 }
diff --git a/chrome/browser/ui/ash/launcher/launcher_context_menu.h b/chrome/browser/ui/ash/launcher/launcher_context_menu.h
index 20385ca..9d9ad6a2 100644
--- a/chrome/browser/ui/ash/launcher/launcher_context_menu.h
+++ b/chrome/browser/ui/ash/launcher/launcher_context_menu.h
@@ -8,10 +8,15 @@
 #include "ash/launcher/launcher_alignment_menu.h"
 #include "ash/launcher/launcher_types.h"
 #include "base/basictypes.h"
+#include "base/memory/scoped_ptr.h"
 #include "ui/base/models/simple_menu_model.h"
 
 class ChromeLauncherController;
 
+namespace extensions {
+class ContextMenuMatcher;
+}
+
 // Context menu shown for a launcher item.
 class LauncherContextMenu : public ui::SimpleMenuModel,
                             public ui::SimpleMenuModel::Delegate {
@@ -58,6 +63,8 @@
 
   ash::LauncherAlignmentMenu alignment_menu_;
 
+  scoped_ptr<extensions::ContextMenuMatcher> extension_items_;
+
   DISALLOW_COPY_AND_ASSIGN(LauncherContextMenu);
 };
 
diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi
index 52a8d1b..c752a86 100644
--- a/chrome/chrome_browser_extensions.gypi
+++ b/chrome/chrome_browser_extensions.gypi
@@ -345,6 +345,8 @@
         'browser/extensions/bundle_installer.h',
         'browser/extensions/component_loader.cc',
         'browser/extensions/component_loader.h',
+        'browser/extensions/context_menu_matcher.cc',
+        'browser/extensions/context_menu_matcher.h',
         'browser/extensions/convert_user_script.cc',
         'browser/extensions/convert_user_script.h',
         'browser/extensions/convert_web_app.cc',
diff --git a/chrome/common/extensions/api/context_menus.json b/chrome/common/extensions/api/context_menus.json
index 5a333eb..c4113ea 100644
--- a/chrome/common/extensions/api/context_menus.json
+++ b/chrome/common/extensions/api/context_menus.json
@@ -44,7 +44,8 @@
           },
           "pageUrl": {
             "type": "string",
-            "description": "The URL of the page where the menu item was clicked."
+            "optional": true,
+            "description": "The URL of the page where the menu item was clicked. This property is not set if the click occured in a context where there is no current page, such as in a launcher context menu."
           },
           "frameUrl": {
             "type": "string",
@@ -115,11 +116,11 @@
                 "type": "array",
                 "items": {
                   "type": "string",
-                  "enum": ["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"]
+                  "enum": ["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio", "launcher"]
                 },
                 "minItems": 1,
                 "optional": true,
-                "description": "List of contexts this menu item will appear in. Defaults to ['page'] if not specified."
+                "description": "List of contexts this menu item will appear in. Defaults to ['page'] if not specified. Specifying ['all'] is equivalent to the combination of all other contexts except for 'launcher'. The 'launcher' context is only supported by apps and is used to add menu items to the context menu that appears when clicking on the app icon in the launcher/taskbar/dock/etc. Different platforms might put limitations on what is actually supported in a launcher context menu."
               },
               "onclick": {
                 "type": "function",
@@ -209,7 +210,7 @@
                 "type": "array",
                 "items": {
                   "type": "string",
-                  "enum": ["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"]
+                  "enum": ["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio", "launcher"]
                 },
                 "minItems": 1,
                 "optional": true
diff --git a/chrome/test/data/extensions/context_menus/event_page/test.js b/chrome/test/data/extensions/context_menus/event_page/test.js
index b2762cf..64d7d92d 100644
--- a/chrome/test/data/extensions/context_menus/event_page/test.js
+++ b/chrome/test/data/extensions/context_menus/event_page/test.js
@@ -9,5 +9,6 @@
 });
 
 chrome.contextMenus.onClicked.addListener(function(info, tab) {
+  chrome.test.assertTrue(tab.id != null);
   chrome.test.sendMessage("onClicked fired for " + info.menuItemId);
 });