編輯:關於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和設備管理器去做的,也是目前比較通用的,而我這個,感覺還是要和我一樣做一些平台性相關的工作才好實用,不然通配性應該是一個問題
在使用UC-WebBrowser時,你會發現它的彈出菜單跟系統自帶的菜單不一樣。它實現更多菜單選項的顯示和分欄。其實,它的本身是PopupWindow或者是AlertDi
在很多地方我們都會用到縱向列表樣式的菜單,比如微信首頁的我、發現頁面,微博的首頁的我頁面,QQ的動態頁面等等等等,大多數的應用中都會存在這樣的頁面。我們怎樣實現這種頁面比
期待已久的MIUI 8終於上線啦!經過全新設計的MIUI 8靈感來源於變幻萬千的“萬花筒”,在色彩、交互動畫、系統字體等方面的大膽改
我們來講一下自定義組合控件,相信大家也接觸過自定義組合控件吧,話不多說,直接干(哈~哈~):大家看到這個覺得這不是很簡單的嗎,這不就是寫個布局文件就搞定嘛,沒錯,確實直接