| // Copyright 2015 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. |
| |
| // This file is for non-chromeos (win & linux) functions, such as |
| // chrome.input.ime.activate, chrome.input.ime.createWindow and |
| // chrome.input.ime.onSelectionChanged. |
| // TODO(azurewei): May refactor the code structure by using delegate or |
| // redesign the API to remove this platform-specific file in the future. |
| |
| #include "chrome/browser/extensions/api/input_ime/input_ime_api.h" |
| |
| #include "base/command_line.h" |
| #include "base/macros.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/input_method/input_method_engine.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/extensions/api/input_ime.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "ui/base/ime/ime_bridge.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| namespace input_ime = extensions::api::input_ime; |
| namespace OnCompositionBoundsChanged = |
| extensions::api::input_ime::OnCompositionBoundsChanged; |
| using ui::IMEEngineHandlerInterface; |
| using input_method::InputMethodEngine; |
| using input_method::InputMethodEngineBase; |
| |
| namespace input_ime = extensions::api::input_ime; |
| |
| namespace { |
| |
| const char kErrorAPIDisabled[] = |
| "The chrome.input.ime API is not supported on the current platform"; |
| const char kErrorNoActiveEngine[] = "The extension has not been activated."; |
| const char kErrorPermissionDenied[] = "User denied permission."; |
| const char kErrorCouldNotFindActiveBrowser[] = |
| "Cannot find the active browser."; |
| const char kErrorNotCalledFromUserAction[] = |
| "This API is only allowed to be called from a user action."; |
| |
| // A preference determining whether to hide the warning bubble next time. |
| // Not used for now. |
| const char kPrefWarningBubbleNeverShow[] = "skip_ime_warning_bubble"; |
| |
| // A preference to see whether the API has never been called, or it's the first |
| // time to call since loaded the extension. |
| // This is used from make an exception for user_gesture checking: no need the |
| // check when restarting chrome. |
| const char kPrefNeverActivatedSinceLoaded[] = "never_activated_since_loaded"; |
| |
| // A preference to see whether the extension is the last active extension. |
| const char kPrefLastActiveEngine[] = "last_activated_ime_engine"; |
| |
| bool IsInputImeEnabled() { |
| return !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisableInputImeAPI); |
| } |
| |
| class ImeObserverNonChromeOS : public ui::ImeObserver { |
| public: |
| ImeObserverNonChromeOS(const std::string& extension_id, Profile* profile) |
| : ImeObserver(extension_id, profile) {} |
| |
| ~ImeObserverNonChromeOS() override {} |
| |
| void OnCompositionBoundsChanged( |
| const std::vector<gfx::Rect>& bounds) override { |
| if (extension_id_.empty() || bounds.empty() || |
| !HasListener(OnCompositionBoundsChanged::kEventName)) |
| return; |
| |
| std::vector<input_ime::Bounds> bounds_list; |
| for (const auto& bound : bounds) { |
| input_ime::Bounds bounds_value; |
| bounds_value.left = bound.x(); |
| bounds_value.top = bound.y(); |
| bounds_value.width = bound.width(); |
| bounds_value.height = bound.height(); |
| bounds_list.push_back(std::move(bounds_value)); |
| } |
| |
| std::unique_ptr<base::ListValue> args( |
| OnCompositionBoundsChanged::Create(bounds_list)); |
| |
| DispatchEventToExtension( |
| extensions::events::INPUT_IME_ON_COMPOSITION_BOUNDS_CHANGED, |
| OnCompositionBoundsChanged::kEventName, std::move(args)); |
| } |
| |
| private: |
| // ImeObserver overrides. |
| void DispatchEventToExtension( |
| extensions::events::HistogramValue histogram_value, |
| const std::string& event_name, |
| std::unique_ptr<base::ListValue> args) override { |
| if (!IsInputImeEnabled()) { |
| return; |
| } |
| |
| std::unique_ptr<extensions::Event> event( |
| new extensions::Event(histogram_value, event_name, std::move(args))); |
| event->restrict_to_browser_context = profile_; |
| extensions::EventRouter::Get(profile_) |
| ->DispatchEventToExtension(extension_id_, std::move(event)); |
| } |
| |
| std::string GetCurrentScreenType() override { return "normal"; } |
| |
| DISALLOW_COPY_AND_ASSIGN(ImeObserverNonChromeOS); |
| }; |
| |
| } // namespace |
| |
| namespace extensions { |
| |
| InputMethodEngine* GetActiveEngine(content::BrowserContext* browser_context, |
| const std::string& extension_id) { |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| InputImeEventRouter* event_router = GetInputImeEventRouter(profile); |
| InputMethodEngine* engine = |
| event_router ? static_cast<InputMethodEngine*>( |
| event_router->GetActiveEngine(extension_id)) |
| : nullptr; |
| return engine; |
| } |
| |
| void InputImeAPI::OnExtensionLoaded(content::BrowserContext* browser_context, |
| const Extension* extension) { |
| // No-op if called multiple times. |
| ui::IMEBridge::Initialize(); |
| |
| // Set the preference kPrefNeverActivatedSinceLoaded true to indicate |
| // input.ime.activate API has been never called since loaded. |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| ExtensionPrefs::Get(profile)->UpdateExtensionPref( |
| extension->id(), kPrefNeverActivatedSinceLoaded, |
| new base::FundamentalValue(true)); |
| } |
| |
| void InputImeAPI::OnExtensionUnloaded(content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionInfo::Reason reason) { |
| InputImeEventRouter* event_router = |
| GetInputImeEventRouter(Profile::FromBrowserContext(browser_context)); |
| if (event_router) { |
| // Records the extension is not the last active IME engine. |
| ExtensionPrefs::Get(Profile::FromBrowserContext(browser_context)) |
| ->UpdateExtensionPref(extension->id(), kPrefLastActiveEngine, |
| new base::FundamentalValue(false)); |
| event_router->DeleteInputMethodEngine(extension->id()); |
| } |
| } |
| |
| void InputImeAPI::OnListenerAdded(const EventListenerInfo& details) {} |
| |
| InputImeEventRouter::InputImeEventRouter(Profile* profile) |
| : InputImeEventRouterBase(profile), active_engine_(nullptr) {} |
| |
| InputImeEventRouter::~InputImeEventRouter() { |
| if (active_engine_) |
| DeleteInputMethodEngine(active_engine_->GetExtensionId()); |
| } |
| |
| InputMethodEngineBase* InputImeEventRouter::GetActiveEngine( |
| const std::string& extension_id) { |
| return (ui::IMEBridge::Get()->GetCurrentEngineHandler() && |
| active_engine_ && |
| active_engine_->GetExtensionId() == extension_id) |
| ? active_engine_ |
| : nullptr; |
| } |
| |
| void InputImeEventRouter::SetActiveEngine(const std::string& extension_id) { |
| // Records the extension is the last active IME engine. |
| ExtensionPrefs::Get(GetProfile()) |
| ->UpdateExtensionPref(extension_id, kPrefLastActiveEngine, |
| new base::FundamentalValue(true)); |
| if (active_engine_) { |
| if (active_engine_->GetExtensionId() == extension_id) { |
| active_engine_->Enable(std::string()); |
| ui::IMEBridge::Get()->SetCurrentEngineHandler(active_engine_); |
| return; |
| } |
| // Records the extension is not the last active IME engine. |
| ExtensionPrefs::Get(GetProfile()) |
| ->UpdateExtensionPref(active_engine_->GetExtensionId(), |
| kPrefLastActiveEngine, |
| new base::FundamentalValue(false)); |
| DeleteInputMethodEngine(active_engine_->GetExtensionId()); |
| } |
| |
| std::unique_ptr<input_method::InputMethodEngine> engine( |
| new input_method::InputMethodEngine()); |
| std::unique_ptr<InputMethodEngineBase::Observer> observer( |
| new ImeObserverNonChromeOS(extension_id, GetProfile())); |
| engine->Initialize(std::move(observer), extension_id.c_str(), GetProfile()); |
| engine->Enable(std::string()); |
| active_engine_ = engine.release(); |
| ui::IMEBridge::Get()->SetCurrentEngineHandler(active_engine_); |
| } |
| |
| void InputImeEventRouter::DeleteInputMethodEngine( |
| const std::string& extension_id) { |
| if (active_engine_ && active_engine_->GetExtensionId() == extension_id) { |
| active_engine_->Disable(); |
| ui::IMEBridge::Get()->SetCurrentEngineHandler(nullptr); |
| delete active_engine_; |
| active_engine_ = nullptr; |
| } |
| } |
| |
| // static |
| bool InputImeActivateFunction::disable_bubble_for_testing_ = false; |
| |
| ExtensionFunction::ResponseAction InputImeActivateFunction::Run() { |
| if (!IsInputImeEnabled()) |
| return RespondNow(Error(kErrorAPIDisabled)); |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| InputImeEventRouter* event_router = GetInputImeEventRouter(profile); |
| if (!event_router) |
| return RespondNow(Error(kErrorNoActiveEngine)); |
| |
| ExtensionPrefs* prefs = ExtensionPrefs::Get(profile); |
| |
| bool never_activated_since_loaded = false; |
| bool last_active_ime_engine = false; |
| |
| if (prefs->ReadPrefAsBoolean(extension_id(), kPrefNeverActivatedSinceLoaded, |
| &never_activated_since_loaded) && |
| never_activated_since_loaded && |
| prefs->ReadPrefAsBoolean(extension_id(), kPrefLastActiveEngine, |
| &last_active_ime_engine) && |
| last_active_ime_engine) { |
| // If the extension is the last active IME engine, and the API is called at |
| // loading the extension, we can tell the API is called from restarting |
| // chrome. No need for user gesture checking. |
| event_router->SetActiveEngine(extension_id()); |
| ExtensionPrefs::Get(profile)->UpdateExtensionPref( |
| extension_id(), kPrefNeverActivatedSinceLoaded, |
| new base::FundamentalValue(false)); |
| return RespondNow(NoArguments()); |
| } |
| // The API has already been called at least once. |
| ExtensionPrefs::Get(profile)->UpdateExtensionPref( |
| extension_id(), kPrefNeverActivatedSinceLoaded, |
| new base::FundamentalValue(false)); |
| |
| // Otherwise, this API is only allowed to be called from a user action. |
| if (!user_gesture()) |
| return RespondNow(Error(kErrorNotCalledFromUserAction)); |
| |
| // Disable using the warning bubble for testing. |
| if (disable_bubble_for_testing_) { |
| event_router->SetActiveEngine(extension_id()); |
| return RespondNow(NoArguments()); |
| } |
| |
| // Disables the warning bubble since we don't need run-time checking anymore. |
| bool warning_bubble_never_show = true; |
| if (warning_bubble_never_show) { |
| // If user allows to activate the extension without showing the warning |
| // bubble, sets the active engine directly. |
| // Otherwise, the extension will be activated when the user presses the 'OK' |
| // button on the warning bubble. |
| event_router->SetActiveEngine(extension_id()); |
| return RespondNow(NoArguments()); |
| } |
| |
| // TODO(azurewei): Remove the warning bubble related codes. |
| Browser* browser = chrome::FindLastActiveWithProfile(profile); |
| if (!browser) |
| return RespondNow(Error(kErrorCouldNotFindActiveBrowser)); |
| |
| // Creates and shows the warning bubble. The ImeWarningBubble is self-owned, |
| // it deletes itself when closed. |
| browser->window()->ShowImeWarningBubble( |
| extension(), |
| base::Bind(&InputImeActivateFunction::OnPermissionBubbleFinished, this)); |
| return RespondLater(); |
| } |
| |
| void InputImeActivateFunction::OnPermissionBubbleFinished( |
| ImeWarningBubblePermissionStatus status) { |
| if (status == ImeWarningBubblePermissionStatus::DENIED || |
| status == ImeWarningBubblePermissionStatus::ABORTED) { |
| // Fails to activate the extension. |
| Respond(Error(kErrorPermissionDenied)); |
| return; |
| } |
| |
| DCHECK(status == ImeWarningBubblePermissionStatus::GRANTED || |
| status == ImeWarningBubblePermissionStatus::GRANTED_AND_NEVER_SHOW); |
| |
| // Activates this extension if user presses the 'OK' button. |
| Profile* profile = Profile::FromBrowserContext(browser_context()); |
| InputImeEventRouter* event_router = GetInputImeEventRouter(profile); |
| if (!event_router) { |
| Respond(Error(kErrorNoActiveEngine)); |
| return; |
| } |
| event_router->SetActiveEngine(extension_id()); |
| |
| if (status == ImeWarningBubblePermissionStatus::GRANTED_AND_NEVER_SHOW) { |
| // Updates the extension preference if user checks the 'Never show this |
| // again' check box. So we can activate the extension directly next time. |
| ExtensionPrefs::Get(profile)->UpdateExtensionPref( |
| extension_id(), kPrefWarningBubbleNeverShow, |
| new base::FundamentalValue(true)); |
| } |
| |
| Respond(NoArguments()); |
| } |
| |
| ExtensionFunction::ResponseAction InputImeDeactivateFunction::Run() { |
| if (!IsInputImeEnabled()) |
| return RespondNow(Error(kErrorAPIDisabled)); |
| |
| InputMethodEngine* engine = |
| GetActiveEngine(browser_context(), extension_id()); |
| ui::IMEBridge::Get()->SetCurrentEngineHandler(nullptr); |
| if (engine) |
| engine->CloseImeWindows(); |
| return RespondNow(NoArguments()); |
| } |
| |
| ExtensionFunction::ResponseAction InputImeCreateWindowFunction::Run() { |
| if (!IsInputImeEnabled()) |
| return RespondNow(Error(kErrorAPIDisabled)); |
| |
| // Using input_ime::CreateWindow::Params::Create() causes the link errors on |
| // Windows, only if the method name is 'createWindow'. |
| // So doing the by-hand parameter unpacking here. |
| // TODO(shuchen,rdevlin.cronin): investigate the root cause for the link |
| // errors. |
| const base::DictionaryValue* params = nullptr; |
| args_->GetDictionary(0, ¶ms); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| input_ime::CreateWindowOptions options; |
| input_ime::CreateWindowOptions::Populate(*params, &options); |
| |
| gfx::Rect bounds(0, 0, 100, 100); // Default bounds. |
| if (options.bounds.get()) { |
| bounds.set_x(options.bounds->left); |
| bounds.set_y(options.bounds->top); |
| bounds.set_width(options.bounds->width); |
| bounds.set_height(options.bounds->height); |
| } |
| |
| InputMethodEngine* engine = |
| GetActiveEngine(browser_context(), extension_id()); |
| if (!engine) |
| return RespondNow(Error(kErrorNoActiveEngine)); |
| |
| int frame_id = engine->CreateImeWindow( |
| extension(), render_frame_host(), |
| options.url.get() ? *options.url : url::kAboutBlankURL, |
| options.window_type == input_ime::WINDOW_TYPE_FOLLOWCURSOR |
| ? ui::ImeWindow::FOLLOW_CURSOR |
| : ui::ImeWindow::NORMAL, |
| bounds, &error_); |
| if (!frame_id) |
| return RespondNow(Error(error_)); |
| |
| std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue()); |
| result->Set("frameId", new base::FundamentalValue(frame_id)); |
| |
| return RespondNow(OneArgument(std::move(result))); |
| } |
| |
| ExtensionFunction::ResponseAction InputImeShowWindowFunction::Run() { |
| if (!IsInputImeEnabled()) |
| return RespondNow(Error(kErrorAPIDisabled)); |
| |
| InputMethodEngine* engine = |
| GetActiveEngine(browser_context(), extension_id()); |
| if (!engine) |
| return RespondNow(Error(kErrorNoActiveEngine)); |
| |
| std::unique_ptr<api::input_ime::ShowWindow::Params> params( |
| api::input_ime::ShowWindow::Params::Create(*args_)); |
| EXTENSION_FUNCTION_VALIDATE(params.get()); |
| engine->ShowImeWindow(params->window_id); |
| return RespondNow(NoArguments()); |
| } |
| |
| ExtensionFunction::ResponseAction InputImeHideWindowFunction::Run() { |
| if (!IsInputImeEnabled()) |
| return RespondNow(Error(kErrorAPIDisabled)); |
| |
| InputMethodEngine* engine = |
| GetActiveEngine(browser_context(), extension_id()); |
| if (!engine) |
| return RespondNow(Error(kErrorNoActiveEngine)); |
| |
| std::unique_ptr<api::input_ime::HideWindow::Params> params( |
| api::input_ime::HideWindow::Params::Create(*args_)); |
| EXTENSION_FUNCTION_VALIDATE(params.get()); |
| engine->HideImeWindow(params->window_id); |
| return RespondNow(NoArguments()); |
| } |
| |
| } // namespace extensions |