blob: 2e924b2278ec29200a01e099d2261b5cd03c2631 [file] [log] [blame]
// Copyright 2010-2014, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package org.mozc.android.inputmethod.japanese.util;
import org.mozc.android.inputmethod.japanese.MozcLog;
import org.mozc.android.inputmethod.japanese.MozcUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import android.annotation.TargetApi;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.IBinder;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
/**
* Factory class for IME switching.
*
* Typical usage ;
* {@code
* ImeSwitcher switcher = ImeSwitcherFactory.getImeSwitcher(inputMethodService);
* boolean isVoiceAvailable = switcher.isVoiceImeAvailable();
* if (isVoiceAvailable) {
* switcher.switchToVoiceIme("ja");
* }
* }
*
*/
public class ImeSwitcherFactory {
static final int SWITCH_NEXT_TARGTET_API_LEVEL = 16;
private static final String GOOGLE_PACKAGE_ID_PREFIX = "com.google.android";
private static final String VOICE_IME_MODE = "voice";
/**
* A container of an InputMethodInfo and an InputMethodSubtype.
*/
private static class InputMethodSubtypeInfo {
private final InputMethodInfo inputMethodInfo;
private final InputMethodSubtype subtype;
public InputMethodSubtypeInfo(
InputMethodInfo inputMethodInfo, InputMethodSubtype subtype) {
if (inputMethodInfo == null || subtype == null) {
throw new NullPointerException("All the prameters must be non-null");
}
this.inputMethodInfo = inputMethodInfo;
this.subtype = subtype;
}
public InputMethodInfo getInputMethodInfo() {
return inputMethodInfo;
}
public InputMethodSubtype getSubtype() {
return subtype;
}
}
/**
* A class to switch default (==current) input method subtype.
*/
public interface ImeSwitcher {
/**
* Returns true if at least one voice IME is available.
*
* Eligible voice IME subtype must match following conditions.
* <ul>
* <li>The voice IME's id has prefix "com.google.android" (for security and avoiding from
* activating unexpected IME).
* <li>The subtype's mode is "voice".
* <li>The subtype is auxiliary (usually a user wants to get back to this IME when (s)he
* types back key).
* </ul>
*
* You don't have to call this method so frequently. Just call after IME's activation.
* The only way to make available an IME is to use system preference screen and when a user
* returns from it to an application the IME receives onStartInput.
*/
public boolean isVoiceImeAvailable();
/**
* Switches to voice ime if possible
*
* @param locale preferred locale. This method uses the subtype of which locale is given one
* but this is best effort.
* @return true if target subtype is found. Note that even if switching itself fails this method
* might return true.
*/
boolean switchToVoiceIme(String locale);
/**
* Switches to *next* IME (including subtype).
*
* @param onlyCurrentIme if true this method find next candidates which are in this IME.
* @return true if the switching succeeds
*/
boolean switchToNextInputMethod(boolean onlyCurrentIme);
}
/**
* A switcher for later OS where IME subtype is available.
*/
static class SubtypeImeSwitcher implements ImeSwitcher {
interface InputMethodManagerProxy {
Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes();
void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype);
}
protected final InputMethodService inputMethodService;
private final InputMethodManagerProxy inputMethodManagerProxy;
public SubtypeImeSwitcher(final InputMethodService inputMethodService) {
this(inputMethodService, new InputMethodManagerProxy() {
InputMethodManager inputMethodManager = MozcUtil.getInputMethodManager(inputMethodService);
@Override
public Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes() {
return inputMethodManager.getShortcutInputMethodsAndSubtypes();
}
@Override
public void setInputMethodAndSubtype(IBinder token,
String id, InputMethodSubtype subtype) {
inputMethodManager.setInputMethodAndSubtype(token, id, subtype);
}
});
}
@VisibleForTesting
SubtypeImeSwitcher(InputMethodService inputMethodService,
InputMethodManagerProxy inputMethodManagerProxy) {
this.inputMethodService = inputMethodService;
this.inputMethodManagerProxy = inputMethodManagerProxy;
}
protected IBinder getToken() {
return inputMethodService.getWindow().getWindow().getAttributes().token;
}
/**
* @param locale if null locale test is omitted (for performance)
* @return null if no candidate is found
*/
private InputMethodSubtypeInfo getVoiceInputMethod(String locale) {
InputMethodSubtypeInfo fallBack = null;
for (Map.Entry<InputMethodInfo, List<InputMethodSubtype>> inputmethod :
inputMethodManagerProxy.getShortcutInputMethodsAndSubtypes().entrySet()) {
InputMethodInfo inputMethodInfo = inputmethod.getKey();
if (!inputMethodInfo.getComponent().getPackageName().startsWith(GOOGLE_PACKAGE_ID_PREFIX)) {
continue;
}
for (InputMethodSubtype subtype : inputmethod.getValue()) {
if (!subtype.isAuxiliary() || !subtype.getMode().equals(VOICE_IME_MODE)) {
continue;
}
if (locale == null || subtype.getLocale().equals(locale)) {
return new InputMethodSubtypeInfo(inputMethodInfo, subtype);
}
fallBack = new InputMethodSubtypeInfo(inputMethodInfo, subtype);
}
}
return fallBack;
}
@Override
public boolean isVoiceImeAvailable() {
// Here we don't take account of locale. Any voice IMEs are acceptable.
return getVoiceInputMethod(null) != null;
}
@Override
public boolean switchToVoiceIme(String locale) {
InputMethodSubtypeInfo inputMethod = getVoiceInputMethod(locale);
if (inputMethod == null) {
return false;
}
inputMethodManagerProxy.setInputMethodAndSubtype(
getToken(),
inputMethod.getInputMethodInfo().getId(),
inputMethod.getSubtype());
return true;
}
@Override
public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
return false;
}
}
/**
* A switcher for much later OS where switchToNextInputMethod is available.
*/
@TargetApi(SWITCH_NEXT_TARGTET_API_LEVEL)
static class NextInputSwitchableImeSwitcher extends SubtypeImeSwitcher {
public NextInputSwitchableImeSwitcher(InputMethodService inputMethodService) {
super(inputMethodService);
}
@Override
public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
return MozcUtil.getInputMethodManager(inputMethodService)
.switchToNextInputMethod(getToken(), onlyCurrentIme);
}
}
// A constructor of concrete switcher class.
// Null if reflection fails.
static final Constructor<? extends ImeSwitcher> switcherConstructor;
static {
Class<? extends ImeSwitcher> clazz;
if (Build.VERSION.SDK_INT >= SWITCH_NEXT_TARGTET_API_LEVEL) {
clazz = NextInputSwitchableImeSwitcher.class;
} else {
clazz = SubtypeImeSwitcher.class;
}
Constructor<? extends ImeSwitcher> constructor = null;
try {
constructor = clazz.getConstructor(InputMethodService.class);
} catch (NoSuchMethodException e) {
MozcLog.e("No ImeSwitcher's constructor found.", e);
}
switcherConstructor = constructor;
}
/**
* Gets an {@link ImeSwitcher}.
*/
public static ImeSwitcher getImeSwitcher(InputMethodService inputMethodService) {
Preconditions.checkNotNull(inputMethodService);
if (switcherConstructor != null) {
try {
return switcherConstructor.newInstance(inputMethodService);
} catch (IllegalArgumentException e) {
MozcLog.e(e.getMessage(), e);
} catch (InstantiationException e) {
MozcLog.e(e.getMessage(), e);
} catch (IllegalAccessException e) {
MozcLog.e(e.getMessage(), e);
} catch (InvocationTargetException e) {
MozcLog.e(e.getMessage(), e);
}
}
return new SubtypeImeSwitcher(inputMethodService);
}
}