乍聽起來,靜默安裝是很是流氓的一件事,它讓用戶不知覺的狀況下被「收割」。可是技術自己是中立的,咱們只談談實現靜默安裝這件事兒。java
下面我將介紹三種靜默安裝的方案,每種方案各有利弊,可是目的是一致的。node
衆所周知,手機被 Root 後能夠作好多奇妙的事情,好比靜默安裝,直接調用 pm install 命令就能夠實現,來看代碼:android
public static boolean silentInstall(String apkPath) {
boolean result = false;
DataOutputStream dataOutputStream = null;
BufferedReader errorStream = null;
BufferedReader successStream = null;
Process process = null;
try {
// 申請 su 權限
process = Runtime.getRuntime().exec("su");
dataOutputStream = new DataOutputStream(process.getOutputStream());
// 執行 pm install 命令
String command = "pm install -r " + apkPath + "\n";
dataOutputStream.write(command.getBytes(Charset.forName("UTF-8")));
dataOutputStream.writeBytes("exit\n");
dataOutputStream.flush();
process.waitFor();
errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
StringBuilder errorMsg = new StringBuilder();
String line;
while ((line = errorStream.readLine()) != null) {
errorMsg.append(line);
}
log.debug("silent install error message:{}", errorMsg);
StringBuilder successMsg = new StringBuilder();
successStream = new BufferedReader(new InputStreamReader(process.getInputStream()));
// 讀取命令執行結果
while ((line = successStream.readLine()) != null) {
successMsg.append(line);
}
log.debug("silent install success message:{}", successMsg);
// 若是執行結果中包含 Failure 字樣就認爲是操做失敗,不然就認爲安裝成功
if (!(errorMsg.toString().contains("Failure") || successMsg.toString().contains("Failure"))) {
result = true;
}
} catch (Exception e) {
log.error(e);
} finally {
try {
if (process != null) {
process.destroy();
}
if (dataOutputStream != null) {
dataOutputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (successStream != null) {
successStream.close();
}
} catch (Exception e) {
// ignored
}
}
return result;
}
public static boolean isRoot() {
return new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
}
複製代碼
首先申請 Root 權限,而後執行 pm install- r <apk 路徑>
命令,-r 參數表示容許覆蓋安裝。 process.waitFor() 說明安裝過程是同步的,等待命令執行完成,而後讀取執行結果。注意:不要在主線程調用靜默安裝的代碼,安裝過程會比較耗時,致使線程一直等待結果。面試
結論:只要手機被 Root,該方法十分奏效。可是絕大部分用戶不懂 Root,即便手機被 Root了,還須要用戶受權,因此該方案侷限性很是大。瀏覽器
當咱們選擇手動安裝應用時,會跳轉到應用安裝界面,這個界面就是系統的 PackageInstaller 提供,專門用來讓用戶有感知地安裝應用。性能優化
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(new File("/sdcard/news.apk")));
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
複製代碼
分析 PackageInstaller 的源碼,咱們發現它會經過 PackageManager 調用 installPackage 方法,這是個隱藏的抽象方法,實現類是 ApplicationPackageManager。主要看一下四個參數:packageURI 就是 apk 的路徑;observer 是安裝的監聽器,應用安裝完成時會被回調,不能爲 null;flags 是標誌位,指定安裝的參數;installersPackageName 表示可選的安裝來源,好比應用寶之類的。bash
public abstract void installPackage(
Uri packageURI, PackageInstallObserver observer,
int flags, String installerPackageName);
複製代碼
ApplicationPackageManager 裏面 mPM 是一個 IPackageManager 類型的對象,它會執行具體的安裝任務。架構
try {
mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,
verificationParams, null);
} catch (RemoteException ignored) {
}
複製代碼
ContextImpl 的 getPackageManager 方法,經過 ActivityThread 獲取 IPackageManager 對象用來構造 ApplicationPackageManager,而後返回 ApplicationPackageManager。app
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; } 複製代碼
ActivityThread 的 getPackageManager 方法,其實就是獲取系統服務的過程。ide
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}
複製代碼
經過以上分析,咱們經過 PackageManager 調用 installPackage 方法就好了,下面看代碼:
public static boolean silentInstall(PackageManager packageManager, String apkPath) {
Class<?> pmClz = packageManager.getClass();
try {
if (Build.VERSION.SDK_INT >= 21) {
Class<?> aClass = Class.forName("android.app.PackageInstallObserver");
Constructor<?> constructor = aClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object installObserver = constructor.newInstance();
Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, aClass, int.class, String.class);
method.setAccessible(true);
method.invoke(packageManager, Uri.fromFile(new File(apkPath)), installObserver, 2, null);
} else {
Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), int.class, String.class);
method.setAccessible(true);
method.invoke(packageManager, Uri.fromFile(new File(apkPath)), null, 2, null);
}
return true;
} catch (Exception e) {
log.error(e);
}
return false;
}
複製代碼
因爲 PackageManager 在不一樣版本上的 installPackage 方法參數不一致,因此咱們根據編譯版本作了處理。在 API 21 及以上,須要傳遞一個非 null 的 PackageInstallObserver,這個類是不可見 的,咱們就用反射建立一個 observer 對象,flags 指定 INSTALL_REPLACE_EXISTING
,用常量表示就是 2。在 API 21 如下,observer 類型是IPackageInstallObserver,一樣使用反射處理便可。
最後聲明權限 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
,還要使用系統簽名,這個很是關鍵,要否則就會出現異常: java.lang.SecurityException: Neither user 10052 nor current process has android.permission.INSTALL_PACKAGES.
。
結論:經過調用系統 API 靜默安裝,終於能夠堂堂正正地搞事情了!雖然這是官方提供的接口,可是爲了避免讓你隨心所欲,強制使用系統簽名,因此對於第三方應用採用的可能性是零。
如今大多數應用採起的是這種辦法,讓用戶主動打開輔助功能,而後模擬點擊用戶操做,進行自動安裝。核心就是 AccessibilityService,咱們來實現這一功能。
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:canRetrieveWindowContent="true"
android:description="@string/accessibility_service_description"
android:packageNames="com.android.packageinstaller"
/>
複製代碼
accessibilityEventTypes: 指定咱們在監聽窗口中能夠模擬哪些事件,typeAllMask 表示全部的事件都能模擬。 accessibilityFeedbackType: 指定無障礙服務的反饋方式。 canRetrieveWindowContent: 指定是否容許咱們的程序讀取窗口中的節點和內容,固然是 true。 description: 當用戶手動配置服務時,顯示給用戶看的說明信息。 packageNames: 指定要監聽哪一個應用程序下的窗口活動,這裏寫 com.android.packageinstaller 表示監聽 Android 系統的安裝界面。 配置裏面描述的內容
<resources>
<string name="app_name">InstallTest</string>
<string name="accessibility_service_description">智能安裝服務,無需用戶的任何操做就能夠自動安裝程序。</string>
</resources>
複製代碼
public class MyAccessibilityService extends AccessibilityService {
private static final String TAG = "[TAG]";
private Map<Integer, Boolean> handleMap = new HashMap<>();
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo nodeInfo = event.getSource();
if (nodeInfo != null) {
int eventType = event.getEventType();
if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
if (handleMap.get(event.getWindowId()) == null) {
boolean handled = iterateNodesAndHandle(nodeInfo);
if (handled) {
handleMap.put(event.getWindowId(), true);
}
}
}
}
}
@Override
public void onInterrupt() {
}
// 遍歷節點,模擬點擊安裝按鈕
private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
if (nodeInfo != null) {
int childCount = nodeInfo.getChildCount();
if ("android.widget.Button".equals(nodeInfo.getClassName())) {
String nodeCotent = nodeInfo.getText().toString();
Log.d(TAG, "content is: " + nodeCotent);
if ("安裝".equals(nodeCotent) || "完成".equals(nodeCotent) || "肯定".equals(nodeCotent)) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return true;
}
}
// 遇到 ScrollView 的時候模擬滑動一下
else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
}
for (int i = 0; i < childCount; i++) {
AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
if (iterateNodesAndHandle(childNodeInfo)) {
return true;
}
}
}
return false;
}
}
複製代碼
當進入安裝界面就會回調 onAccessibilityEvent() 這個方法,咱們只處理 TYPE_WINDOW_CONTENT_CHANGED 和 TYPE_WINDOW_STATE_CHANGED 兩個事件。爲了防止重複處理事件,用 map 來過濾事件,而後遞歸遍歷節點,找到「安裝」、「完成」、「缺點」的按鈕就模擬點擊。因爲安裝界面須要用戶看完權限纔出現按鈕,因此遇到 ScrollView 的時候就模擬滾動,直到出現安裝按鈕。
<service
android:name=".MyAccessibilityService"
android:label="智能安裝應用"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
>
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config"
/>
</service>
複製代碼
android:label:這個就是用戶看到的無障礙服務的名稱。 android:permission: 須要用到 BIND_ACCESSIBILITY_SERVICE 這個權限。 action: android.accessibilityservice.AccessibilityService 有了這個 action,用戶才能在設置裏面看到咱們的服務,不然用戶沒法開啓咱們的 AccessibilityService,也就不能進到 MyAccessibilityService 裏面了。
private void smartInstall() {
Uri uri = Uri.fromFile(new File("/sdcard/test.apk"));
Intent localIntent = new Intent(Intent.ACTION_VIEW);
localIntent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(localIntent);
}
複製代碼
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivity(intent);
複製代碼
智能安裝是一種妥協的方案,在沒有 Root 和安裝權限的狀況下,確實解放了用戶的拇指。看看市面上的應用,大部分都採用了這種方法。應用市場使用智能安裝能夠理解,視頻瀏覽器工具一類不相干的應用也要開啓?我真是呵呵了。
在如今這個金三銀四的面試季,我本身在網上也蒐集了不少資料作成了文檔和架構視頻資料免費分享給你們【包括高級UI、性能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料】,但願能幫助到您面試前的複習且找到一個好的工做,也節省你們在網上搜索資料的時間來學習。