編輯:關於Android編程
最近自家的系統要做一個升級服務,裡面有三個功能,第一個是系統升級,也就是下載OTA包推送到recovery裡升級的,而第二個是MCU升級,這就涉及到我們自家系統的一些情況了,而第三個就是應用升級了,領導要求不要騷擾用戶,於是我就想到了靜默安裝了,因為我們的系統是在wifi環境下工作的,所以不擔心流量哈,而且我們系統是沒有ROOT的,所以我們肯定野不能使用RunTime方式去推送到data/app下,那我們要怎麼做呢?幾經思考,於是找了比較多的資料,看了挺多的文章,於是自己實現了這個功能,現在把經驗也總結出來了,於是就有了本篇博文,好了,我們一起來分析到實現這個功能吧!
1.純手工,點擊安裝包,安裝應用 2.adb安裝我們安裝應用引出來的思考,我們正常情況下,應該是怎麼去安裝一個應用?
//安裝 adb install xxx.apk //卸載 adb uninstall xxx.apk
其實大多數的應用,比如360,應用寶,都是采用命令的方式去實現的,比如
pm install -r
http://androidxref.com而我們要想搞清楚,那就得去看下源碼是怎麼實現的了,在我們源碼目錄的frameworks/base/cmds/pm工程裡,這裡推薦一個在線查看源碼的網站
http://androidxref.com/4.0.3_r1/xref/frameworks/base/cmds/pm/src/com/android/commands/pm/Pm.java而我們的Pm.java地址在
這是一個java文件,我們安裝其實就是執行了這個java工程,我們找到他的main,可以看到,他其實就是執行了一個run方法,而我們在源碼的98-106行可以看到
98 if ("install".equals(op)) {
99 runInstall();
100 return;
101 }
102
103 if ("uninstall".equals(op)) {
104 runUninstall();
105 return;
106 }
這個就是我們安裝和卸載所執行的方法,而我們這裡重點來看一下安裝,他所執行的方法runInstall在源碼的743-812行
743 private void runInstall() {
744 int installFlags = 0;
745 String installerPackageName = null;
746
747 String opt;
748 while ((opt=nextOption()) != null) {
749 if (opt.equals("-l")) {
750 installFlags |= PackageManager.INSTALL_FORWARD_LOCK;
751 } else if (opt.equals("-r")) {
752 installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
753 } else if (opt.equals("-i")) {
754 installerPackageName = nextOptionData();
755 if (installerPackageName == null) {
756 System.err.println("Error: no value specified for -i");
757 showUsage();
758 return;
759 }
760 } else if (opt.equals("-t")) {
761 installFlags |= PackageManager.INSTALL_ALLOW_TEST;
762 } else if (opt.equals("-s")) {
763 // Override if -s option is specified.
764 installFlags |= PackageManager.INSTALL_EXTERNAL;
765 } else if (opt.equals("-f")) {
766 // Override if -s option is specified.
767 installFlags |= PackageManager.INSTALL_INTERNAL;
768 } else {
769 System.err.println("Error: Unknown option: " + opt);
770 showUsage();
771 return;
772 }
773 }
774
775 final Uri apkURI;
776 final Uri verificationURI;
777
778 // Populate apkURI, must be present
779 final String apkFilePath = nextArg();
780 System.err.println("\tpkg: " + apkFilePath);
781 if (apkFilePath != null) {
782 apkURI = Uri.fromFile(new File(apkFilePath));
783 } else {
784 System.err.println("Error: no package specified");
785 showUsage();
786 return;
787 }
788
789 // Populate verificationURI, optionally present
790 final String verificationFilePath = nextArg();
791 if (verificationFilePath != null) {
792 System.err.println("\tver: " + verificationFilePath);
793 verificationURI = Uri.fromFile(new File(verificationFilePath));
794 } else {
795 verificationURI = null;
796 }
797
798 PackageInstallObserver obs = new PackageInstallObserver();
799 try {
800 mPm.installPackageWithVerification(apkURI, obs, installFlags, installerPackageName,
801 verificationURI, null);
802
803 synchronized (obs) {
804 while (!obs.finished) {
805 try {
806 obs.wait();
807 } catch (InterruptedException e) {
808 }
809 }
810 if (obs.result == PackageManager.INSTALL_SUCCEEDED) {
811 System.out.println("Success");
812 } else {
813 System.err.println("Failure ["
814 + installFailureToString(obs.result)
815 + "]");
816 }
817 }
818 } catch (RemoteException e) {
819 System.err.println(e.toString());
820 System.err.println(PM_NOT_RUNNING_ERR);
821 }
822 }
這個方法就是安裝了,不過我們也可以不去關注這個方法,我們只要關注他精髓的一行代碼
mPm.installPackageWithVerification(apkURI, obs, installFlags, installerPackageName,verificationURI, null);
http://androidxref.com/2.1/xref/frameworks/base/cmds/pm/src/com/android/commands/pm/Pm.java這行代碼位於800-801行,他最終執行的也就是這行代碼,其實就是installPackageWithVerification方法,所以,我們如果調用這個方法,是不是也是可以直接安裝而不用去走界面安裝的流程?這裡要注意一下,我們在4.0之前的方法不是這個哦
但是原理都是一樣的
mPm.installPackage(Uri.fromFile(new File(apkFilePath)), obs, installFlags,installerPackageName);
而我們要調用installPackage這個方法,就需要使用mPm,那mPm是個什麼東西呢?
IPackageManager mPm;
他是一個AIDL的接口,他初始化的內容
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
而正常情況下,我們是無法調用的,如果我們想實現這一點的話,我們就要拿到系統服務中的IPackageManager,我們要怎麼做?肯定是實現我們的AIDL
http://androidxref.com/2.1/xref/frameworks/base/core/java/android/content/pm/IPackageManager.aidl我們通過AIDL去實現我們的靜默安裝,這是有必要的,那我們去哪裡找這個aidl文件?
那我們新建一個工程,去導入他,在我們的Android中怎麼去做呢?在main裡面新建一個aidl文件,同時,我們新建一個包名:android.content.pm,然後把IPackageManager.aidl拷貝進去

裡面的內容就不多說了,我們sync一下,你就會看到

說明我們還需要這幾個引用的aidl,於是我們找啊找,找到之後再次sync一下,他又提示我們需要一些aidl了

這一步其實不麻煩,只是我寫的步驟分開了而已,我們要一步步去實現是吧,於是我們又繼續的找啊找,終於把他所需要的aidl文件全部給拷貝進來了,我們現在可以測試一下

可以看到,我們可以使用IPackageManager了呢,那好,小司機們,我們現在就可以去嘗試的干點什麼有趣的事情了
好的,我們仿照我們上面的runInstall方法來實現我們的靜默安裝,一起代碼邏輯請參考Pm.java,我們先寫個布局
OK,那我們就去實現了,我們要做的很簡單,就是我們比如把qq.apk放在sd卡根目錄,然後我們再輸入框上輸入一個應用名,點擊靜默安裝,就去執行,是不是很簡單,那好,我們邏輯是這樣的,但是我們還是會碰到一些問題的,比如我們要去初始化IPackageManager的時候,源碼中是這樣子的
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
但是當我們去做的時候,就比較尴尬的發現

我們拿不到ServiceManager,那我們要怎麼才能拿到呢?其實說實在的,這個也是不難的,我們可以通過反射去實現,有了思路,我們就去嘗試一下
//反射獲取ServiceManager
try {
//指定反射類
Class forName = Class.forName("android.os.ServiceManager");
//獲取方法,參數是String類型
Method method = forName.getMethod("getService", String.class);
//傳入參數
IBinder iBinder = (IBinder) method.invoke(null, "package");
//初始化AIDL
mPm = IPackageManager.Stub.asInterface(iBinder);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
現在我們可以直接調用installPackage方法了
/**
* 安裝apk
*
* @param apkPath 路徑
*/
private void runInstall(String apkPath) {
/**
* install方法
* 1.uri:安裝文件路徑
* 2.observer:觀察者,安裝成功還是失敗
* 3.flags:標記狀態
* 4.installer: 整個路徑
*/
try {
mPm.installPackage(Uri.fromFile(new File(apkPath)),
new PackInstallObserver(), INSTALL_REPLACE_EXISTING,
new File(apkPath).getPath());
} catch (RemoteException e) {
e.printStackTrace();
}
}
裡面的幾個參數看一下就明白了,但是現在安裝確實會失敗的,他會提示我們沒有權限去做這件事,要求我們加上這個權限
但是我們加上之後他還是不通過,他說我們不是系統的應用程序,於是我們就要想辦法做成系統的應用程序了
我們首先在manifest根節點添加uid

http://androidxref.com/2.1/xref/build/target/product/security/然後我們把這個應用先打包簽名,怎麼簽名就不說了,簽名之後,我們再去源碼裡找這幾樣東西
platform.pk8目錄下的
platform.x509.pem
http://androidxref.com/2.1/xref/build/tools/signapk/
SignApk.java目錄下的
我們把這幾個文件放在一起,然後使用命令

簽完名之後我們可以看到NewApk.apk

到這裡我們算是實現了,看下完整的代碼
package com.liuguilin.silentinstall;
import android.content.pm.IPackageInstallObserver;
import android.content.pm.IPackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 靜默安裝 --by 劉桂林
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//輸入框
private EditText etPackageNmae;
//執行按鈕
private Button btnInstall;
//安裝路徑
private String path = Environment.getExternalStorageDirectory().getAbsolutePath();
//安裝類
private IPackageManager mPm;
//install flags 狀態,詳見PackageManager
private static final int INSTALL_REPLACE_EXISTING = 0X00000002;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
/**
* 初始化View
*/
private void initView() {
//反射獲取ServiceManager
try {
Class forName = Class.forName("android.os.ServiceManager");
Method method = forName.getMethod("getService", String.class);
IBinder iBinder = (IBinder) method.invoke(null, "package");
//初始化AIDL
mPm = IPackageManager.Stub.asInterface(iBinder);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//初始化
etPackageNmae = (EditText) findViewById(R.id.etPackageNmae);
btnInstall = (Button) findViewById(R.id.btnInstall);
btnInstall.setOnClickListener(this);
}
/**
* 點擊事件
*
* @param view
*/
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btnInstall:
String apkPath = path + "/" + etPackageNmae.getText().toString().trim();
runInstall(apkPath);
break;
}
}
/**
* 安裝apk
*
* @param apkPath 路徑
*/
private void runInstall(String apkPath) {
/**
* install方法
* 1.uri:安裝文件路徑
* 2.observer:觀察者,安裝成功還是失敗
* 3.flags:標記狀態
* 4.installer: 整個路徑
*/
try {
mPm.installPackage(Uri.fromFile(new File(apkPath)),
new PackInstallObserver(), INSTALL_REPLACE_EXISTING,
new File(apkPath).getPath());
} catch (RemoteException e) {
e.printStackTrace();
}
}
/**
* 觀察者
*/
class PackInstallObserver extends IPackageInstallObserver.Stub {
@Override
public void packageInstalled(String packageName, int returnCode) throws RemoteException {
//根據returnCode判斷是否成功失敗
}
}
}
這路要說明的幾點,我在自家平台使用了framework.jar哦,但是覺得原理是通用的,在Andorid Studio上終究是有一些問題,如果在Eclipse上應該就方便很多了,好了我們本篇的思考就到這裡,他的做法是利用了ROOT和設備管理器去做的,也是目前比較通用的,而我這個,感覺還是要和我一樣做一些平台性相關的工作才好實用,不然通配性應該是一個問題
Android之用PopupWindow實現彈出菜單的方法詳解
在使用UC-WebBrowser時,你會發現它的彈出菜單跟系統自帶的菜單不一樣。它實現更多菜單選項的顯示和分欄。其實,它的本身是PopupWindow或者是AlertDi
RecyclerView實現常見的列表菜單
在很多地方我們都會用到縱向列表樣式的菜單,比如微信首頁的我、發現頁面,微博的首頁的我頁面,QQ的動態頁面等等等等,大多數的應用中都會存在這樣的頁面。我們怎樣實現這種頁面比
MIUI 8和MIUI 7有什麼區別 MIUI 8和MIUI 7區別對比
期待已久的MIUI 8終於上線啦!經過全新設計的MIUI 8靈感來源於變幻萬千的“萬花筒”,在色彩、交互動畫、系統字體等方面的大膽改
Android自定義控件之組合控件學習筆記分享
我們來講一下自定義組合控件,相信大家也接觸過自定義組合控件吧,話不多說,直接干(哈~哈~):大家看到這個覺得這不是很簡單的嗎,這不就是寫個布局文件就搞定嘛,沒錯,確實直接