前一阵子,应泰国客户需求,需要在Android TV系统定制一个多语言输入法,至少支持中、英、泰三种语言。拿到这个任务,对于至今还是小白的我来说,当然先去google一下有没有大神专门做过符合要求的输入法应用。很遗憾,网上移动终端倒是有不少满足需求的输入法,而且做得还满酷炫,当时搜到的最全面的最接近需求(包含泰语这种名不见经传的小语种)的输入法应用当属Go Keyboard,后来发现我的HTC one手机自带的HTC Sense Input输入法也满足要求,而且还很纯净,系统原生的无广告,符合Material Design风格,但是能在TV上用的还真没找到(口碑不错的搜狗输入法倒是也做了TV版,可惜只支持中英文切换)。
对于程序员来说,开发是你的本职,没有开发过的东西,对我们恰好是机遇,况且使用第三方输入法,毕竟控制权不在自己手里,用户使用出了问题,也修复不了bug,源码拿不到啊!不过,这次的需求是定制系统输入法,不是做软键盘,在时间精力有限情况下,让一个菜鸟短时间内开发出来不太现实,那怎么办呢,这是后话。我们先看看需求出现的客观原因吧!
分析原因,Android TV起步较晚是一方面,操作方式跟手机不一样是根本原因,不同于手机touch,TV是通过遥控控制焦点来执行用户操作。所以,凡是不支持焦点控制的移动端应用,在TV上要么用不了,要么用户体验差(目前TV BOX支持鼠标、键盘操作),更有存在因分辨率引起的显示问题。当然有需求就会有市场,为什么没人在TV输入法模块投入精力去开发呢?原因很简单,遥控输入法真心不好用,至于原因,想想操作方式,再拿个遥控实际体验一下就知道了,输个密码都很艰难,谁还用?况且有人开发了远程输入法,即手机跟盒子在同一个网关环境下,通过手机输入、TV负责显示输出的方式,相当于手机作为遥控使,很方便。还有更方便的,直接把遥控做成键盘,使用硬键盘输入模式。
既然这样,我后面也就不用写下去了,因为我做的工作已经失去市场价值。但是我还想说两句,毕竟在完成任务过程中,我得到了不少工作经验,想跟大家分享一下。可能有人疑问,老板为什么还要让你开发这样一个市场价值不高的东西呢,原因很简单,公司要降低产品开发成本,前面提到的已有开发技术会增加成本投入,毕竟价格定高了,卖不出去,这个我们不多谈吧,作为员工,踏踏实实完成任务就行。
现在进入正题,前面说,实现这个需求既不是靠第三方应用集成,也不是自己完整去开发,那还有第三条路吗?当然有,那就是从系统源码出发,作为一个已经很成熟的系统,只要不是很变态的功能,你都能通过定制修改编译源码来达到目的。我们知道Android源码中默认的有三种输入法:英文、中文、日文,对应的工程代码路径为:
<android_root>/packages/inputmethods/LatinIME/
<android_root>/packages/inputmethods/PinyinIME/
<android_root>/packages/inputmethods/OpenWnn/
其中Latin输入法支持的语种最多,可惜唯独不支持中文输入,没关系,我们可以曲线救国,不是还有一个拼音输入法吗!现在思路有了,实现要分两步:
第一步:解决输入法焦点问题
也就是修改原生输入法,使之支持TV操作,具体实现过程参见我上上篇博客:Android TV定制输入法
第二步:解决多输入法切换问题
前面很明了,我们要满足需求,系统得集成两种输入法:LatinIME和PinyinIME,那就牵涉到多输入法应用切换问题,这个问题我们不能交给用户去处理,Android默认输入法是LatinIME,当用户使用的系统语言环境是英语和泰语等语言时好说,使用中文时,就要让输入法切换成拼音输入法了。按照这样的思路,那只需要在切换系统语言的代码段里加入切换输入法函数就行了,我开始也是这么想的,但是没调试通。
参考了Android输入法之——如何在代码中强制切换输入法_ccwwff的博客-CSDN博客_android 切换输入法实现这篇博客,没解决问题,可能Android新版本API变了,后来看到了另一种方法:
Settings.Secure.putString( mContext.getContentResolver(),Settings.Secure.DEFAULT_INPUT_METHOD,myIME );
倒是起作用,但是有个新问题,系统语言切换后,系统管理输入法的服务类InputMethodManagerService.java会强制切换默认输入法为LatinIME,如果这个类执行重置默认输入法方法在我切换输入法方法后面,那我的代码编写的切换输入法动作就会被覆盖。
问题的关键是如何控制我的设置默认输入法函数在系统那个方法后面执行,这个新问题出现后,首先想到的解决方法是写一个监听系统语言切换广播,通过广播来控制代码执行时机,不过可惜的是,未能如愿解决问题,系统语言切换后,重置默认输入法并不是立即执行,具体什么时候执行,还得深入研究。
那换一个思路吧,加个Handler消息延时发送,问题看起来好像解决了,因为调试后,切换输入法的确成功了,但是偶尔还会失败,通过Log打印发现,有时系统重置默认输入法方法还是会跑到我的切换函数后面执行。按照常规思路,把延时加长不就行了?但是增加延时值会导致系统响应操作变慢,况且这样做也不符合程序健壮性,只能另辟蹊径了!
还是从源码出发,细细研究InputMethodManagerService.java这个类,里面有个resetDefaultImeLocked方法,他是设置系统默认输入法的,我们就从这里入手,加一个限制条件:当系统语言为中文时,设置默认输入法为Pinyin输入法,到这里,问题貌似已经解决了。但是修改源码是有风险的,首先你无法保证你修改的东西会不会带来不可预知的问题,毕竟源码是经过时间考验的。其次,你修改的东西只是针对某个项目,可能其他方案就是要用原生的,所以这里还得加标志位,把修改带来的影响减小到最低,这里就要用到Android的属性系统(System Property)了。
这里简要介绍一下有关Android System Property:
顾名思义系统属性,肯定对整个系统全局共享。通常程序的执行以进程为单位各自相互独立,如何实现全局共享呢?System Properties是怎么一回事,又是如何实现的呢?
属性系统是android的一个重要特性。它作为一个服务运行,管理系统配置和状态。所有这些配置和状态都是属性。每个属性是一个键值对(key/value pair),其类型都是字符串。这些属性可能是有些资源的使用状态,进程的执行状态,系统的特有属性……
代码中大量存在SystemProperties.set()/SystemProperties.get();通过这两个接口可以对系统的属性进行读取/设置。
可以通过命令adb shell :getprop查看手机上所有属性状态值,或者getprop init.svc.bootanim制定查看某个属性状态,使用setprop init.svc.bootanim start设置某个属性的状态。
特别属性 :
那在本问题中如何使用该属性系统呢?我们可以自定义以“persist.”开头的属性,如果你的项目引入了layoutlib.jar包,可以直接调用android.os.SystemProperties,因为这个类是加了{@hide}标签的,如果是普通apk,需要通过Java反射机制调用。
package com.gotech.tv.launcher.util;
/**
* @author john
* @created 2016-2-19
*/
import java.lang.reflect.Method;
public class SystemPropertiesUtil
{
public static String get(String key)
{
String ret = null;
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("get", new Class[] { String.class });
mthd.setAccessible(true);
Object obj = mthd.invoke(clazz, new Object[] { key });
if (obj != null && obj instanceof String)
{
ret = (String) obj;
}
}
catch (Exception e)
{
e.printStackTrace();
}
return ret;
}
public static String get(String key, String def)
{
String ret = def;
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("get", new Class[] { String.class, String.class });
mthd.setAccessible(true);
Object obj = mthd.invoke(clazz, new Object[] { key, def });
if (obj != null && obj instanceof String)
{
ret = (String) obj;
}
}
catch (Exception e)
{
e.printStackTrace();
}
return ret;
}
public static boolean getBoolean(String key, boolean def)
{
boolean ret = def;
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("getBoolean", new Class[] { String.class, boolean.class });
mthd.setAccessible(true);
Object obj = mthd.invoke(clazz, new Object[] { key, def });
if (obj != null && obj instanceof Boolean)
{
ret = (Boolean) obj;
}
}
catch (Exception e)
{
e.printStackTrace();
}
return ret;
}
public static int getInt(String key, int def)
{
int ret = def;
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("getInt", new Class[] { String.class, int.class });
mthd.setAccessible(true);
Object obj = mthd.invoke(clazz, new Object[] { key, def });
if (obj != null && obj instanceof Integer)
{
ret = (Integer) obj;
}
}
catch (Exception e)
{
e.printStackTrace();
}
return ret;
}
public static long getLong(String key, long def)
{
long ret = def;
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("getLong", new Class[] { String.class, long.class });
mthd.setAccessible(true);
Object obj = mthd.invoke(clazz, new Object[] { key, def });
if (obj != null && obj instanceof Long)
{
ret = (Long) obj;
}
}
catch (Exception e)
{
e.printStackTrace();
}
return ret;
}
public static void set(String key, String value)
{
try
{
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method mthd = clazz.getMethod("set", new Class[] { String.class, String.class });
mthd.setAccessible(true);
mthd.invoke(clazz, new Object[] { key, value });
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
下面就是在项目中自定义一个系统属性,我们在启动Activity的onCreate()里添加:
// john add for default IME setting
SystemProperties.set("persist.sys.sync.ime" , "true");
最后贴上源码修改部分,在InputMethodManagerService.java的resetDefaultImeLocked方法添加:
private void resetDefaultImeLocked(Context context) {
// Do not reset the default (current) IME when it is a 3rd-party IME
if (mCurMethodId != null
&& !InputMethodUtils.isSystemIme(mMethodMap.get(mCurMethodId))) {
return;
}
InputMethodInfo defIm = null;
for (InputMethodInfo imi : mMethodList) {
if (defIm == null) {
if (InputMethodUtils.isValidSystemDefaultIme(
mSystemReady, imi, context)) {
defIm = imi;
Slog.i(TAG, "Selected default: " + imi.getId());
}
}
}
if (defIm == null && mMethodList.size() > 0)
{
//john add for sync system language and input method -->
Slog.i(TAG, "persist.sys.sync.ime : " +SystemProperties.get("persist.sys.sync.ime", "false")+"************Language : "+mRes.getConfiguration().locale.getLanguage());
if (SystemProperties.get("persist.sys.sync.ime", "false").equals("true") && mRes.getConfiguration().locale.getLanguage().equals("zh"))
{
for (InputMethodInfo imi : mMethodList)
{
if (imi.getId().equals(REMOTE_IME))
{
defIm = imi;
Slog.i(TAG, "Custom default : " + defIm.getId());
}
}
}// <--end
else
{
defIm = InputMethodUtils.getMostApplicableDefaultIME(mSettings.getEnabledInputMethodListLocked());
Slog.i(TAG, "No default found, using " + defIm.getId());
}
}
if (defIm != null) {
setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false);
}
}
附上我们自定义默认输入法的ID:
/**
* john add for Chinese Input
*/
private static final String REMOTE_IME="com.hisilicon.android.inputmethod.remote/.RemoteIME";