Android TV 源码修改默认输入法
前一陣子,應泰國客戶需求,需要在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設置某個屬性的狀態。
特別屬性 :
- 如果屬性名稱以“ro.”開頭,那么這個屬性被視為只讀屬性。一旦設置,屬性值不能改變。
- 如果屬性名稱以“persist.”開頭,當設置這個屬性時,其值也將寫入/data/property。
- 如果屬性名稱以“net.”開頭,當設置這個屬性時,“net.change”屬性將會自動設置,以加入到最后修改的屬性名。(這是很巧妙的,netresolve模塊的使用這個屬性來追蹤在net.*屬性上的任何變化。)
- 屬性“ ctrl.start ”和“ ctrl.stop ”是用來啟動和停止服務。每一項服務必須在/init.rc中定義.系統啟動時,與init守護進程將解析init.rc和啟動屬性服務。一旦收到設置“ ctrl.start ”屬性的請求,屬性服務將使用該屬性值作為服務名找到該服務,啟動該服務。這項服務的啟動結果將會放入“ init.svc.<服務名>“屬性中。客戶端應用程序可以輪詢那個屬性值,以確定結果。
那在本問題中如何使用該屬性系統呢?我們可以自定義以“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 settingSystemProperties.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 IMEif (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());}}}// <--endelse{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";總結
以上是生活随笔為你收集整理的Android TV 源码修改默认输入法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AI做了什么,才能惊艳到你?
- 下一篇: 将coreldraw文档以html格式,