安卓使用Root權限實現後臺模擬全局按鍵、觸屏事件方法(類似按鍵精靈)

有時我們需要使用安卓實現在後臺模擬系統按鍵,比如對音量進行調節(模擬音量鍵),關閉前臺正在運行的App(模擬返回鍵),或者模擬觸屏事件。但是對於原生安卓系統而言,後臺進程關閉前臺進程,甚至模擬用戶事件,進而操控整個系統,是不符合系統安全原則的,如果有這樣的漏洞被病毒或惡意軟件所利用,會非常危險。
 
由於一些特殊原因,我恰巧需要實現這樣的功能,而又沒有條件自行編譯安卓系統(但是可以利用Root權限,因爲Root權限的獲取相對簡單很多,並且很多用戶的安卓設備都有Root過)。網上也看到很多人在提類似的問題,很多人討論了半天,結果都是無解。於是我花了很大精力,最後終於找到了解決方案。
 
在網上查找了很多資料,主要找到兩種方法:Instrumentation和IWindowManager。
 
 

使用Instrumentation接口:對於非自行編譯的安卓系統,無法獲取系統簽名,只能在前臺模擬按鍵,不能後臺模擬。

 
一種是使用Instrumentation接口,這個接口原本是用來對軟件進行測試而留出來的。經過嘗試,發現這個接口可以模擬按鍵,但是前提是在應用處於前臺時。而應用處於前臺時,模擬按鍵基本上也沒有太大的作用(模擬按鍵操作應用自身似乎沒有很大意義)。
 
當應用處於後臺時,這個Instrumentation接口就失效了。網上找到的解釋是,在後臺使用這個接口,需要有系統權限,也就是在Manifest中添加android:sharedUserId="android.uid.system"。而這會導致什麼問題呢?聲明瞭系統權限的APK,只有具有系統簽名的情況下,才能被安裝到安卓設備上,比如系統自帶的電話、短信,本質上也就是APK程序,但是這些應用具有系統權限。
 
安卓系統有一套簽名機制,APK只有有了數字簽名,才能被安裝。通常調試時默認Eclipse自動對其進行簽名,使用的是Debug簽名。當發佈應用時,開發者則使用自己獨有的數字簽名文件對APK進行簽名(這個文件可以用Eclipse生成,簽名也可以讓Eclipse完成)。APK有新版本的安裝時,如果檢測到簽名不一致,系統會提示簽名不一致,只有卸載舊版本才能安裝。這一機制從一定程度上避免了第三方對官方發佈的APK進行修改甚至非法植入病毒等行爲(當然如果用戶主動卸載舊版本的官方應用、安裝新版本的非官方APK也是可以的)。而具有同一簽名的不同App,它們之間可以共享一些數據。
 
而系統簽名怎麼獲取呢?在編譯安卓系統的時候,會將一個系統簽名的數字簽名文件放到一起編譯。對於一個已經編譯完成的系統,或者爲了適配不同系統,必然無法獲取到這個數字簽名文件,於是也無法對APK進行系統簽名。最後就導致具有uid.system屬性的APK無法被安裝,於是Instrumentation接口後臺模擬按鍵的方法,只能在自行編譯系統的情況下纔可以使用。
 
 

使用反射方法調用系統IWindowManager隱藏API:兼容性較差,穩定性不好,容易出錯。另外實際編譯時發生錯誤,原因暫時不明。

 
網上還有一種方法。安卓系統中有一些隱藏API,通常是利用Java的權限限制,使得這些API無法被調用。但通過反射的方式,可以突破Java的權限限制。在IWindowManager中就隱藏了可以模擬按鍵和觸屏事件的API。嘗試網上的方法,下載到一個由安卓源碼編譯好的jar文件,添加到工程中,然後使用發射編寫了一些代碼,嘗試調用隱藏API。結果編譯的時候Eclipse直接不響應了,可能是因爲電腦配置不夠,jar文件太大。嘗試了幾次沒有成功,又考慮到這種方法有很多弊端,並且很可能最後還是需要系統權限(網上不少文章說得不是很清楚),於是就放棄了這個方法。
 
android模擬按鍵問題總結[使用IWindowManager.injectKeyEvent方法]
 
 
 

JNI調用C程序模擬按鍵:仍然是權限問題。

 
參考了網上一些資料所提出的可能的思路,發現剩下能想到的方法就是用JNI實現,通過調用C/C++程序來模擬按鍵。對Linux底層編程不熟悉,網上參考了一些代碼,在Ubuntu下編寫了一個按鍵模擬程序,很順利的編譯運行通過。然後又開始學習JNI的編譯方法,先在C程序層寫了個簡單的加法運算,編譯運行測試通過,然後就把模擬按鍵的代碼貼了進去。滿懷期待的寫好安卓Java層代碼,編譯、下載、執行程序,卻發現完全沒有效果。
 
想看一下到底是哪一步出錯了,就在C程序裏面改了改,用LogCat打印出C程序的返回值,發現在打開按鍵設備的時候出錯,看來肯定又是權限的問題了。
 
儘管系統已經Root,APK也允許使用Root權限,但是Root權限沒法傳遞給C程序,權限不夠,程序無法執行。在網上找了一通有關Linux、安卓權限的資料,也沒找出來什麼思路。其實當時很疑惑,在Linux系統中,Root權限是最高的權限,安卓也不例外,有文章指出,Root權限>系統權限>用戶權限。儘管能獲取到Root權限,卻不能完成系統權限所能完成的任務,總感覺不應該。
 
 

安卓按鍵精靈:使用Root權限而不需系統簽名,實現後臺模擬按鍵和觸屏等事件是可行的。

 
當時很絕望,感覺估計只有自行編譯系統才能解決問題了。就在那時候,突然想起了按鍵精靈軟件。以前用過電腦版,在安卓市場一找,果然也有安卓版。下載使用發現,按鍵精靈就可以實現在後臺模擬按鍵操作,需要Root權限,但是是什麼原理卻不得而知。本想嘗試反編譯源碼查看,但是當時出了一些問題,反編譯沒有成功。在網上搜索安卓按鍵精靈的原理,除了之前的那兩種依賴源碼環境才能使用的API,也沒有找到結果。不過至少說明了,使用Root權限而不需要系統簽名,實現模擬按鍵、並且兼容大量安卓設備是可行的。
 
 

最終解決問題:使用Shell調用ADB指令實現。

 
繼續在網上搜索安卓按鍵模擬(其實那時都不知道用什麼關鍵字好了,能想到的關鍵字都用遍了,但是搜索出來的結果,都是之前提到的那幾個依賴源碼環境和系統權限的方案)。發現有很多介紹ADB調試,向手機發送按鍵事件的文章。剛好之前做過在Root權限下,用Java調用安卓底層的Linux Shell,然後執行pm指令進行APK的安裝卸載。這時我突發奇想,能否用Shell調用ADB指令呢?
 
於是就進行了嘗試,使用Java執行Runtime.getRuntime().exec("su").getOutputStream(),獲取了一個具有Root權限的Process的輸出流對象,向其中寫入字符串即可以Root權限被Shell執行,ADB模擬按鍵的指令爲 "input keyevent keyCode",keyCode爲按鍵的鍵值,例如KeyEvent.KEYCODE_VOLUME_UP表示音量加。
 
編譯完程序安裝執行,終於實現了預期的效果,當時非常高興。至於觸屏或鼠標事件,只要調用相應的ADB指令即可。但是有一點問題,就是反應速度非常慢,尤其是連續模擬多個按鍵的時候,甚至會死機。而按鍵精靈運行的就相當流暢,我又開始好奇按鍵精靈是怎麼實現的。
 
後來終於還是找到了原因,模擬按鍵時,不應每次都調用Runtime.getRuntime().exec("su"),因爲每次調用這個代碼的時候,都會獲取Runtime實例,並且執行"su"請求Root權限,反應就會很慢(我的理解是相當於每次都新開一個命令行窗口);而應該只是在一開始執行一次,並獲取一個OutputStream實例,後來每次執行一條Shell指令,只需向其中寫入相應字符串,這樣就快了很多。
 
下面貼出可用的代碼。要求設備已經Root,不需要其他任何特殊權限或簽名。由於用的是ADB指令,兼容性也不會有太大問題。首次運行程序時(其實也就是執行Runtime.exec("su")的時候),會請求Root權限。

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. /** 
  2.  * 用root權限執行Linux下的Shell指令 
  3.  *  
  4.  * @author jzj 
  5.  * @since 2014-09-09 
  6.  */  
  7. public class RootShellCmd {  
  8.   
  9.     private OutputStream os;  
  10.   
  11.     /** 
  12.      * 執行shell指令 
  13.      *  
  14.      * @param cmd 
  15.      *            指令 
  16.      */  
  17.     public final void exec(String cmd) {  
  18.         try {  
  19.             if (os == null) {  
  20.                 os = Runtime.getRuntime().exec("su").getOutputStream();  
  21.             }  
  22.             os.write(cmd.getBytes());  
  23.             os.flush();  
  24.         } catch (Exception e) {  
  25.             e.printStackTrace();  
  26.         }  
  27.     }  
  28.   
  29.     /** 
  30.      * 後臺模擬全局按鍵 
  31.      *  
  32.      * @param keyCode 
  33.      *            鍵值 
  34.      */  
  35.     public final void simulateKey(int keyCode) {  
  36.         exec("input keyevent " + keyCode + "\n");  
  37.     }  
  38. }  

寫這篇文章的主要目的,並不是要強調這件事的難度,也不只是爲了提出問題的解決方案(那樣就沒必要寫前面那麼多過程了)。而是想把我解決問題的過程完整的寫出來,對我而言算是一個記錄,對讀者而言,沒準能從中找到一些東西。
 
解決這個問題之後,後來意外的發現,這個問題其實有人已經解決了,並且發了博客。不幸的是,那篇博客被大量使用前兩種思路的博客掩埋了,當時我怎麼也沒找到。這篇博客地址在此:
 
 
順便說明一點,這篇博客中作者提到的缺點:反應速度較慢。前面提到我也越到了同樣的問題,也已經給出瞭解決方案。

本文由jzj1993原創,轉載請註明來源:http://www.hainter.com/android-key-simulation
發佈了4 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章