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






评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值