主從式App實現靜默更新及root許可權擴充套件
之前公司一個專案,專案需求為軟體在後臺自動更新,有新版本釋出則自動下載並安裝新版本。通過查閱了大量資料,瞭解了要想完成這件事情途徑有兩:
1, app需要擁有系統級別的身份。這就需要在系統原始碼中獲取到系統簽名,然後對生成的app進行簽名,完了之後才能安裝執行在系統上執行靜默操作;
2, 在已root系統上app獲取到系統root許可權,即可執行靜默安裝的操作。
由於公司的嵌入式裝置已root,那麼這裡我直接選擇了後者。
實現獲取系統root許可權並且進行靜默安裝的過程其實還是比較簡單的,網上也有很多這方面的教程,我這裡就簡單的說下過程:
1, 檢測裝置是否root:
public boolean hasRooted() { if (hasRooted == null) { for (String path : Constants.SU_BINARY_DIRS) { File su = new File(path + "/su"); if (su.exists()) { hasRooted = true; break; } else { hasRooted = false; } } } return hasRooted; }
不同的裝置su所在地址可能不一樣,為了儘量適配所有的裝置這裡把所有可能的地址放在一個數組裡面:
public static final String[] SU_BINARY_DIRS = {
"/system/bin", "/system/sbin", "/system/xbin",
"/vendor/bin", "/sbin"
};
2, 接著獲取系統的root許可權,將會彈出對話方塊讓使用者選擇是否授予此應用root許可權
public boolean grantPermission() { if (!hasGivenPermission) { hasGivenPermission = accessRoot(); lastPermissionCheck = System.currentTimeMillis(); } else { if (lastPermissionCheck < 0 || System.currentTimeMillis() - lastPermissionCheck > Constants.PERMISSION_EXPIRE_TIME) { hasGivenPermission = accessRoot(); lastPermissionCheck = System.currentTimeMillis(); } } return hasGivenPermission; }
3, 接下來執行非同步安裝即可
runAsyncTask(new AsyncTask<Void, Void, Result>() { @Override protected void onPreExecute() { updateLog("Installing package " + apkPath + " ..........."); super.onPreExecute(); } @Override protected Result doInBackground(Void... params) { return RootManager.getInstance().installPackage(apkPath); } @Override protected void onPostExecute(Result result) { updateLog("Install " + apkPath + " " + result.getResult() + " with the message " + result.getMessage()); super.onPostExecute(result); } });
主要方法:
public Result installPackage(String apkPath, String installLocation) {
RootUtils.checkUIThread();//如果是UIthread丟擲異常
final ResultBuilder builder = Result.newBuilder(); //執行結果集
if (TextUtils.isEmpty(apkPath)) {
return builder.setFailed().build();
}
String command = Constants.COMMAND_INSTALL;
if (RootUtils.isNeedPathSDK()) { //4.0版本以上必須在命令前加上patch
command = Constants.COMMAND_INSTALL_PATCH + command;
}
command = command + apkPath;
if (TextUtils.isEmpty(installLocation)) {
if (installLocation.equalsIgnoreCase("ex")) { //安裝至外接記憶體
command = command + Constants.COMMAND_INSTALL_LOCATION_EXTERNAL;
} else if (installLocation.equalsIgnoreCase("in")) { //安裝至內建記憶體
command = command + Constants.COMMAND_INSTALL_LOCATION_INTERNAL;
}
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {//裝載命令
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
String finalInfo = infoSb.toString();
if (TextUtils.isEmpty(finalInfo)) {
builder.setInstallFailed();
} else {
if (finalInfo.contains("success") || finalInfo.contains("Success")) {
builder.setInstallSuccess();
} else if (finalInfo.contains("failed") || finalInfo.contains("FAILED")) {
if (finalInfo.contains("FAILED_INSUFFICIENT_STORAGE")) {
builder.setInsallFailedNoSpace();
} else if (finalInfo.contains("FAILED_INCONSISTENT_CERTIFICATES")) {
builder.setInstallFailedWrongCer();
} else if (finalInfo.contains("FAILED_CONTAINER_ERROR")) {
builder.setInstallFailedWrongCer();
} else {
builder.setInstallFailed();
}
} else {
builder.setInstallFailed();
}
}
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish(); //執行
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
其實獲取了系統的root不僅能執行靜默安裝,還能執行其他有趣的命令,以下是我收集總結的一些命令:
pm install –r 靜默安裝
-s 安裝APP到SD-CARD
-f 安裝APP至phone RAM
pm uninstall 靜默解除安裝
"rm '" + apkPath +"'" 解除安裝系統app
screencap 系統截圖
ps 程序是否執行
"pidof "+程序名 通過程序名殺死一個程序
"kill "+程序ID 通過程序ID殺死一個程序
"reboot -p" 關機
"reboot" 重啟
"reboot recovery" 重啟進入Recovery模式
執行命令方法:
public Result runCommand(String command) {
final ResultBuilder builder = Result.newBuilder();
if (TextUtils.isEmpty(command)) {
return builder.setFailed().build();
}
final StringBuilder infoSb = new StringBuilder();
Command commandImpl = new Command(command) {
@Override
public void onUpdate(int id, String message) {
infoSb.append(message + "\n");
}
@Override
public void onFinished(int id) {
builder.setCustomMessage(infoSb.toString());
}
};
try {
Shell.startRootShell().add(commandImpl).waitForFinish();
} catch (InterruptedException e) {
e.printStackTrace();
builder.setCommandFailedInterrupted();
} catch (IOException e) {
e.printStackTrace();
builder.setCommandFailed();
} catch (TimeoutException e) {
e.printStackTrace();
builder.setCommandFailedTimeout();
} catch (PermissionException e) {
e.printStackTrace();
builder.setCommandFailedDenied();
}
return builder.build();
}
繼續回到主題《靜默安裝》,你以為這樣就完了,其實還沒,這樣做是能執行安裝的操作,但是安裝的不是本身,而是其他app,就是說靜默安裝執行者的執行物件不能是本身,既然如此,那就必須得藉助外部的力量才能對自己完成更新,so,便引入了主、從APP的概念,這裡我們把這個需要更新的app作為主APP,然後再新增一個從APP,讓從APP對主APP執行一個安裝更新的操作就行了,思略良久,一個大致的流程就想出來了:
首先主APP首次安裝即把攜帶的從APP靜默安裝至系統,並啟動,讓其在後臺執行,當主APP接收到伺服器發來的更新指令,則下載新版主APP,然後啟動從APP,若啟動過程中發現系統中的從APP被誤刪或者第一次未安裝成功則再次執行安裝,安裝完成後就啟動從APP,然後主APP通知從APP執行靜默安裝操作,由於是兩個APP之間的通訊,這裡就採用了常規的通訊方式——廣播,通過傳送一條安裝廣播,從APP接收到就對預先下載好的apk進去靜默安裝,如此一來,主APP就完成了軟體自動更新的操作。
//首次安裝拷貝install_quite.apk到sdCard根目錄,並安裝啟動執行,這裡把從APP放在了主APP的Assets目錄下面
if(copyAssetsToFile("install_quite.apk", Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk")){
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk");//執行後臺安裝
}
@Override
protected void onPostExecute(Result result) {
<span style="white-space:pre"> </span> Intent intent = new Intent();
<span style="white-space:pre"> </span> ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
<span style="white-space:pre"> </span> intent.setComponent(cn);
<span style="white-space:pre"> </span> intent.setAction("android.intent.action.MAIN");
try {
startActivity(intent);//啟動從APP
} catch (Exception e) {
}
super.onPostExecute(result);
}
});
}
當接收到伺服器軟體更新的指令,把新版本的apk下載至指定資料夾,然後執行以下啟動從APP,傳送安裝廣播等操作:
<span style="white-space:pre"> </span>final Intent intent1 = new Intent();
ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
intent1.setComponent(cn);
intent1.setAction("android.intent.action.MAIN");
try {
startActivity(intent1); //啟動 靜默安裝從app
} catch (Exception e) { //如果系統沒安裝此 靜默安裝從app,則會進入catch
//把Assets裡的apk拷貝到sdCard,並安裝開啟執行
if(copyAssetsToFile("install_quite.apk", Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk")){
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
Environment.getExternalStorageDirectory().getPath() + "/install_quite.apk");
}
@Override
protected void onPostExecute(Result result) {
Toast.makeText(MainMenu.this, result.getMessage(),Toast.LENGTH_SHORT).show();
try {
startActivity(intent1);
sendBroadcast(new Intent("INSTALL_NEW_PACKAGE"));
} catch (Exception e) {
}
super.onPostExecute(result);
}
});
}
else
Toast.makeText(this, "<span style="font-family: Arial, Helvetica, sans-serif;">Assets</span><span style="font-family: Arial, Helvetica, sans-serif;">內未找到install_quite.apk", Toast.LENGTH_LONG).show();</span>
}
//發廣播則告訴它要對主app進行更新了
sendBroadcast(new Intent("INSTALL_NEW_PACKAGE"));
從APP比較簡單,主要就一個接收廣播和執行操作的service:
public class MainService extends Service {
private BroadcastReceiver myBroadCast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals("INSTALL_NEW_PACKAGE")) {
Toast.makeText(context, "接收到了一條廣播為" + "INSTALL_NEW_PACKAGE",Toast.LENGTH_LONG).show();
runAsyncTask(new AsyncTask<Void, Void, Result>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Result doInBackground(Void... params) {
return RootManager.getInstance().installPackage(
"/sdcard/aaa.apk");
}
@Override
protected void onPostExecute(Result result) {
Toast.makeText(getApplication(), result.getMessage(),Toast.LENGTH_SHORT).show();
startAPP("android_serialport_api.sample");
super.onPostExecute(result);
}
});
}
}
};
@Override
public void onCreate() {
super.onCreate();
}
@Override
public void onDestroy() {
this.unregisterReceiver(myBroadCast);
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//註冊廣播
IntentFilter myFilter = new IntentFilter();
myFilter.addAction("INSTALL_NEW_PACKAGE");
this.registerReceiver(myBroadCast, myFilter);
flags = START_STICKY;
Toast.makeText(getApplication(), "已經開啟service", Toast.LENGTH_LONG).show();
return super.onStartCommand(intent, flags, startId);
}
/*
* 啟動一個app
*/
private void startAPP(String packageName) {
try {
Intent intent = this.getPackageManager().getLaunchIntentForPackage(packageName);
startActivity(intent);
} catch (Exception e) {
Toast.makeText(getApplication(), "沒有安裝", Toast.LENGTH_LONG).show();
}
}
private static final <T> void runAsyncTask(AsyncTask<T, ?, ?> asyncTask,T... params) {
asyncTask.execute(params);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
由於擔心客戶在不明情況的情況下會誤刪該從APP,需要對其圖示進行隱藏,那麼怎麼隱藏其圖示呢,其實很簡單,只要在表單檔案中註釋category即可
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
</intent-filter>
這裡還有另外一種利用程式碼動態隱藏圖示的方式供大家參考:
private void setComponentEnabled(Class<?> clazz, boolean enabled) {
final ComponentName c = new ComponentName(this, clazz.getName());
getPackageManager().setComponentEnabledSetting(c,enabled?PackageManager.COMPONENT_ENABLED_STATE_ENABLED:PackageManager.COMPONENT_E<span style="white-space:pre"> </span>NABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP);
}
細心的朋友可能發現了我上面用了兩種啟動APP方式:即如下兩種
1, Intent intent = new Intent();
ComponentName cn = new ComponentName("com.yph.install_quite","com.yph.install_quite.MainActivity");
intent.setComponent(cn);
intent.setAction("android.intent.action.MAIN");
try {
startActivity(intent);
} catch (Exception e) {
Toast.makeText(this, "沒有該從APP,請下載安裝",Toast.LENGTH_SHORT).show();
}
2,
/**
* 啟動一個app
* @author yph
*/
<span style="white-space:pre"> </span>private void startAPP(String packageName) {
try {
Intent intent = this.getPackageManager().getLaunchIntentForPackage(packageName);
startActivity(intent);
} catch (Exception e) {
}
}
這裡寫說下他們的區別,顯然第二種比較簡單,只需要傳入包名即可,但是其缺陷在於不能啟動沒有設定category的app,即是不能啟動隱藏了圖示的app。
OK,以上便是全部內容。