摘要:
android 静默安装、卸载实现
微信公众号:ty_skyline (天涯工作室)
android中应用的安装卸载,大家(用android设备的)肯定不陌生。这里就来浅谈android应用的安装、卸载的实现方式。
1.系统安装程序
android自带了一个安装程序--/system/app/PackageInstaller.apk.大多数情况下,我们手机上安装应用都是通过这个apk来安装的。代码使用也非常简单:
/* 安装apk */
public static void installApk(Context context, String fileName) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + fileName),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
/* 卸载apk */
public static void uninstallApk(Context context, String packageName) {
Uri uri = Uri.parse("package:" + packageName);
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
context.startActivity(intent);
}
通过发一个Intent,把应用所在的路径封装整uri.之后默认启动了PackageInstaller.apk来安装程序了。但是此种情况下,仅仅是个demo而已,很难达到开发者的需求。如:
- 界面不好
- 什么时候安装完了,卸载完了呢?
为了达到自己的需求,相信很多人都会接着来监听系统安装卸载的广播,继续接下来的代码逻辑。
2.监听系统发出的安装广播
在安装和卸载完后,android系统会发一个广播
- android.intent.action.PACKAGE_ADDED(安装)
- android.intent.action.PACKAGE_REMOVED(卸载)
咱们就监听这广播,来做响应的逻辑处理。实现代码:
public class MonitorSysReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
//接收安装广播
if (intent.getAction().equals("android.intent.action.PACKAGE_ADDED")) {
//TODO
}
//接收卸载广播
if (intent.getAction().equals("android.intent.action.PACKAGE_REMOVED")) {
//TODO
}
}
}
AndroidMenifast.xml里配置:
到此,确实安装卸载的整体流程都知道了,但是这个效果肯定是无法达到项目的需求。
一般这种应用商店类的项目,肯定是会要自定义提示框效果的安装卸载功能,而不是调用系统的安装程序。
那咱就要想法子实现静默安装、卸载咯。
网上有很多法子,如执行adb install 或pm install -r命令安装。但我想这并不可靠。记得之前有做过一个应用来执行linux命令,是通过RunTime来执行命令的。
后来发现其实并不靠谱,还不如直接用C代码来实现。
下面这种调用系统隐藏api接口来实现静默安装卸载,是比较大众靠谱的,实现自定义的提示界面。O(∩_∩)O~
3.系统隐藏的api
隐藏api,顾名思义,普通情况下肯定是调用不到的。翻翻源码\frameworks\base\core\java\android\content\pm目录下PackageManager.java,应该发现在注释行里有加上@hide声明。调用的安装下载接口如下:
public abstract void installPackage(Uri packageURI,
IPackageInstallObserver observer, int flags,
String installerPackageName);
public abstract void deletePackage(String packageName,
IPackageDeleteObserver observer, int flags);
并且都是抽象方法,需要咱们实现。看参数里IPackageInstallObserver observer一个aidl回调通知接口,当前目录中找到这接口:
package android.content.pm;
/**
* API for installation callbacks from the Package Manager.
* @hide
*/
oneway interface IPackageInstallObserver {
void packageInstalled(in String packageName, int returnCode);
}
好吧,这里有现成的干货,咱拿过来直接用呗(当然如果没有源码的那就算了,那能实现的只是demo)。具体步骤:
从源码中拷贝要使用的aidl回调接口:
- IPackageInstallObserver.aidl、IPackageDeleteObserver.aidl当然完全可以拷贝整个pm目录,这样就不会报错了O(∩_∩)O~。
-
作者项目里面用到了pm,所以把PackageManager.java以及涉及到的一些文件也拷贝过来了,不然eclipse报找不到PackageManager对象。结构如下:
(注:此处的包名android.content.pm一定要和源码目录结构一致,不然源码里编译会提示找不到aidl接口。一切朝源码编译看齐)
此处有2种方式实现:
- 直接只取IPackageDeleteObserver.aidl 和IPackagerInstallObserver.aidl、IPackageMoveObserver.aidl等要使用的接口,然后通过bindService来和系统连接服务,然后直接调用接口即可(这种没有方式作者没试过,不过原理上来说应该是可行的,除非系统没有这个Service实现这个接口。有需求的可以深究下)
- 作者此处的方法是直接拷贝了源码PackageManager.java等文件过来,不过靠过来之后eclipse会提示一些接口错误,但这里作者把上面那几个.java文件都放空了,因为用不到,只是为了编译过才拷贝了那么多文件。最简单的就是直接拷贝4个文件即可:
PackageManager.java
IPackageDeleteObserver.aidl
IPackagerInstallObserver.aidl
IPackageMoveObserver.aidl
然后把PackageManager.java中报的异常的接口都注释掉即可
- 实现回调接口,代码如下
class MyPakcageInstallObserver extends IPackageInstallObserver.Stub {
Context cxt;
String appName;
String filename;
String pkname;
public MyPakcageInstallObserver(Context c, String appName,
String filename,String packagename) {
this.cxt = c;
this.appName = appName;
this.filename = filename;
this.pkname = packagename;
}
@Override
public void packageInstalled(String packageName, int returnCode) {
Log.i(TAG, "returnCode = " + returnCode);// 返回1代表安装成功
if (returnCode == 1) {
//TODO
}
Intent it = new Intent();
it.setAction(CustomAction.INSTALL_ACTION);
it.putExtra("install_returnCode", returnCode);
it.putExtra("install_packageName", packageName);
it.putExtra("install_appName", appName); cxt.sendBroadcast(it);
}
}
卸载回调接口同上。
- 调用PackageManager.java隐藏方法,代码如下:
/**
* 静默安装
* */
public static void autoInstallApk(Context context, String fileName,
String packageName, String APPName) {
Log.d(TAG, "jing mo an zhuang:" + packageName + ",fileName:" + fileName);
File file = new File(fileName);
int installFlags = 0;
if (!file.exists())
return;
installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
if (hasSdcard()) {
installFlags |= PackageManager.INSTALL_EXTERNAL;
}
PackageManager pm = context.getPackageManager();
try {
IPackageInstallObserver observer = new MyPakcageInstallObserver(
context, APPName, appId, fileName,packageName,type_name);
Log.i(TAG, "########installFlags:" + installFlags+"packagename:"+packageName);
pm.installPackage(Uri.fromFile(file), observer, installFlags,
packageName);
} catch (Exception e) {
}
}
卸载调用同上
很多码友联系,这里经常出错,现整理参考代码如下(下面代码有些格式问题):
package cn.thear;
import java.io.File;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.IPackageInstallObserver;
import android.content.pm.IPackageMoveObserver;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.os.RemoteException;
import android.util.Log;
public class ApkOperateManager {
public static String TAG = "ApkOperateManager";
/***安装apk */
public static void installApk(Context context, String fileName) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + fileName),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
/**卸载apk */
public static void uninstallApk(Context context, String packageName) {
Uri uri = Uri.parse("package:" + packageName);
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
context.startActivity(intent);
}
/**
* 静默安装
* */
public static void installApkDefaul(Context context, String fileName,
String packageName, String APPName, String appId, String type_name) {
Log.d(TAG, "jing mo an zhuang:" + packageName + ",fileName:" + fileName
+ ",type_name:" + type_name);
File file = new File(fileName);
int installFlags = 0;
if (!file.exists())
return;
installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
if (hasSdcard()) {
installFlags |= PackageManager.INSTALL_EXTERNAL;
}
PackageManager pm = context.getPackageManager();
// try { try {
IPackageInstallObserver observer = new MyPakcageInstallObserver(
context, APPName, appId, fileName, packageName, type_name);
Log.i(TAG, "########installFlags:" + installFlags + "packagename:"
+ packageName);
pm.installPackage(Uri.fromFile(file), observer, installFlags,
packageName);
} catch (Exception e) {
((MarketApplication) context).setApp_detail_status(appId,
MarketApplication.APP_STATUS_NOTEXIT);
}
}
/* 静默卸载 */
public static void uninstallApkDefaul(Context context, String action,
String packageName) {
PackageManager pm = context.getPackageManager();
IPackageDeleteObserver observer = new MyPackageDeleteObserver(context,
action, packageName);
pm.deletePackage(packageName, observer, 0);
}
/* 静默卸载回调 */
private static class MyPackageDeleteObserver extends
IPackageDeleteObserver.Stub {
Context cxt;
String action;
String pkname;
public MyPackageDeleteObserver(Context c, String action, String pkname) {
this.cxt = c;
this.action = action;
this.pkname = pkname;
}
@Override
public void packageDeleted(String packageName, int returnCode) {
Log.d(TAG, "returnCode = " + returnCode + ",action:" + action
+ "packageName:" + packageName + ",pkname:" + pkname);// 返回1代表卸载成功
if (returnCode == 1) {//TODO 以下是删除数据库记录,只做参考
/*SharedPreferences installedAPPInfo = cxt.getSharedPreferences(
"installedAPPInfo", Context.MODE_WORLD_READABLE);
if (installedAPPInfo.contains(packageName)) {
String appId = installedAPPInfo.getString(packageName,
"no this appId");
((MarketApplication) cxt.getApplicationContext())
.setApp_detail_status(appId,
MarketApplication.APP_STATUS_NOTEXIT);
installedAPPInfo.edit().remove(packageName).commit();
ContentResolver conResolver = cxt.getContentResolver();
conResolver.delete(InstalledAppInfo.CONTENT_URI,
InstalledAppInfo.APP_PKNAME + " = " + "'" + pkname
+ "'", null);
}
MarketApplication ma = ((MarketApplication) cxt
.getApplicationContext());
Log.e(TAG, "###packageDeleted###111size:"
+ ma.getManagerLists().size());
ma.removeManagerItem(pkname);
ma.removeUpdateItem(pkname);
Log.e(TAG, "##packageDeleted####22222size:"
+ ma.getManagerLists().size());*/
}
Intent it = new Intent();
it.setAction(action);
it.putExtra("uninstall_returnCode", returnCode);
cxt.sendBroadcast(it);
}
}
/* 静默安装回调 */
private static class MyPakcageInstallObserver extends
IPackageInstallObserver.Stub {
Context cxt;
String appName;
String appId;
String filename;
String pkname;
String type_name;
public MyPakcageInstallObserver(Context c, String appName,
String appId, String filename, String packagename,
String type_name) {
this.cxt = c;
this.appName = appName;
this.appId = appId;
this.filename = filename;
this.pkname = packagename;
this.type_name = type_name;
}
@Override
public void packageInstalled(String packageName, int returnCode) {
MarketApplication ma = ((MarketApplication) cxt
.getApplicationContext());
Log.i(TAG,
"returnCode = " + returnCode + ","
+ ma.getApp_detail_status(appId));// 返回1代表安装成功
Intent it = new Intent();
it.setAction(CustomAction.INSTALL_ACTION);
it.putExtra("install_returnCode", returnCode);
it.putExtra("install_packageName", packageName);
it.putExtra("install_appName", appName);
it.putExtra("install_appId", appId);
if (returnCode == 1) {
//ma.getAPPList();
//ma.setManagerLists();
if (ma.getApp_detail_status(appId) == MarketApplication.APP_STATUS_UPDATITNG) {
ma.removeUpdateItem(pkname);
cxt.sendBroadcast(it);
return;
}
SharedPreferences installedAPPInfo = cxt.getSharedPreferences(
"installedAPPInfo", Context.MODE_WORLD_READABLE);
installedAPPInfo.edit().putString(packageName, appId).commit();
// 保存信息到数据库
if (appId != null && appName != null && pkname != null
&& type_name != null) {
ContentResolver conResolver = cxt.getContentResolver();
ContentValues values = new ContentValues();
values.put(InstalledAppInfo.APP_ID, appId);
values.put(InstalledAppInfo.APP_NAME, appName);
values.put(InstalledAppInfo.APP_PKNAME, pkname);
values.put(InstalledAppInfo.APP_TYPENAME, type_name);
Uri result = conResolver.insert(
InstalledAppInfo.CONTENT_URI, values);
Log.i(TAG,
"#########install suscess...result:"
+ result.toString());
}
ma.setApp_detail_status(appId,
MarketApplication.APP_STATUS_INSTALLED);
}
File f = new File(filename);
if (f.exists()) {
f.delete();
}
cxt.sendBroadcast(it);
}
}
/**
* sd卡不存在
*/
public static final int NO_SDCARD = -1;
/**
* 移动应用到SD Card
*
* @param context
* @param pkname
* @return
*/
public static void movePackage(Context context, String pkname) {
PackageManager pm = context.getPackageManager();
MovePackageObserver mpo = new MovePackageObserver();
pm.movePackage(pkname, mpo, PackageManager.INSTALL_EXTERNAL);
}
/**
* 移动应用的回调
*/
public static class MovePackageObserver extends IPackageMoveObserver.Stub {
public MovePackageObserver() {
}
@Override
public void packageMoved(String packageName, int returnCode)
throws RemoteException {
Log.i(TAG, "packagename:" + packageName + ",returnCode:"
+ returnCode);
}
}
/**
* 判断有无sd卡
* */
public static boolean hasSdcard() {
String status = Environment.getExternalStorageState();
if (status.equals(Environment.MEDIA_MOUNTED)
|| status.equals("/mnt/sdcard")) {
Log.i(TAG, "has sdcard....");
return true;
} else {
return false;
}
}
}
自此,静默安装卸载代码实现。最后在AndroidMenifast.xml中要注册权限和添加为系统用户组,如果eclipse编译的话,并记得签名
...
注:最后特别注意一点,因为下载的apk等只有rw----- root可读写权限,必须用个办法来给下载的apk赋权限,让系统级应用可以打开操作。这里作者是通过一个jni来调用C层接口,实现给指定的apk赋权限。然后执行安装apk过程
permission_change.cpp
#include
#include
#include
#include
#include
#include
#define LOGI printf
#define LOGE printf
static const char *classPathName = "com/utils/PermissionNative"; //此处包名视java层native包名而定
typedef union{
JNIEnv* env;
void* venv;
}UnionJNIEnvToVoid;
static jboolean ChangePermission(const char* str)
{
const char* p;
char tmp_path[50];
memset(tmp_path,0,sizeof(tmp_path));
p = str+5;
while(p < str+strlen(str))
{
if(*p =='/')
{
memcpy(tmp_path,str,p-str);
if(chmod(tmp_path,0755) == -1)
{
LOGI("chmod %s failed!\n",tmp_path);
return JNI_FALSE;//
}
LOGI("tmp_path_chmod = %s\n",tmp_path);
}
p++;
}
if(chmod(str,0755) == -1)
{
LOGI("chmod %s failed!\n",str);
return JNI_FALSE;//
}
return JNI_TRUE;//
}
static jboolean PermissionChange(JNIEnv *env, jobject thiz,jstring path)
{
const char* str;
str = env->GetStringUTFChars(path,false);
if(str == NULL)
{
return JNI_FALSE;
}
if(ChangePermission(str))
{
env->ReleaseStringUTFChars(path,str);
return JNI_TRUE;//JNI_FALSE
} else {
env->ReleaseStringUTFChars(path,str);
return JNI_FALSE;//
}
}
static JNINativeMethod methods[] = {
{"native_permission_change", "(Ljava/lang/String;)Z", (void*)PermissionChange },
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGI("Native registration unable to find class '%s'\n", className);
return JNI_FALSE;
}
LOGI("FindClass succ\n");
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGI("RegisterNatives failed for '%s'\n", className);
return JNI_FALSE;
}
LOGI("RegisterNatives succ\n");
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env,classPathName,
methods, sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
LOGI("registerNatives succ\n");
return JNI_TRUE;
}
// -------------------------------------------------------------------------
/*
* This is called by the VM when the shared library is first loaded.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;
LOGI("JNI_OnLoad begin\n");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
LOGI("ERROR: GetEnv failed\n");
goto bail;
}
LOGI("GetEnv succ\n");
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) {
LOGI("ERROR: registerNatives failed\n");
goto bail;
}
LOGI("registerNatives succ!");
result = JNI_VERSION_1_4;
bail:
return result;
}
Android.mk如下:
LOCAL_PATH :=$(call my-dir)
include $(CLEAR_VARS)
LOCAL_PRELINK_MODULE :=false
LOCAL_MODULE_TAGS := eng
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog
LOCAL_SRC_FILES :=permission_change.cpp
LOCAL_MODULE :=libpermission_change_jni
include $(BUILD_SHARED_LIBRARY)
4.拷贝apk
条件:
1.获取系统权限
2.拷贝apk到data/app
注:4.2和4.0上以测试通过,不过没有回调提示的,需要自己代码实现
5.效果图
最最后,眼见为实:附上效果图(注ddms截图有色差,不知道咋解决,有知道的请告之,万分感谢):
6.补充说明
遇到很多朋友实现静默安装时,来问很多各式各样的问题,这里我说明下:
我只是提供了一种我实现的方式,并不是个公共的模板,大家实现的时候肯定会遇到很多问题,有些编译不过,或者那些类找不到,参数不对等等,我也感到很无力。
这里我只是记录自己工作中实现时大的方向,并且所有相关的代码都在里面。如果有哪些模糊不明白的,欢迎大家来咨询;但是像一些编译问题我觉得大家还是自己解决,并且各个环境不一样,我也不一定能解决。
最后我只能很确切的保证,以上方式是绝对可行的。但是也有局限性,并不是所有平台都能通用的,只能是系统用户组的apk才能调用隐藏接口,并需要签名(不同平台的签名肯定是不一样的)。具体原因可以看我后面的一篇介绍签名的文章