1. 程式人生 > >主從式App實現靜默更新及root許可權擴充套件

主從式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,以上便是全部內容。