android中应用的安装卸载,大家(用android设备的)肯定不陌生。这里就来浅谈android应用的安装、卸载的实现方式。
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而已,很难达到开发者的需求。如:
界面不好
什么时候安装完了,卸载完了呢?
为了达到自己的需求,相信很多人都会接着来监听系统安装卸载的广播,继续接下来的代码逻辑。
在安装和卸载完后,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里配置:
<receiver android:name=".MonitorSysReceiver"> <intent-filter> <action android:name="android.intent.action.PACKAGE_ADDED" /> <action android:name="android.intent.action.PACKAGE_REMOVED" /> </intent-filter> </receiver>
到此,确实安装卸载的整体流程都知道了,但是这个效果肯定是无法达到项目的需求。
一般这种应用商店类的项目,肯定是会要自定义提示框效果的安装卸载功能,而不是调用系统的安装程序。
那咱就要想法子实现静默安装、卸载咯。
网上有很多法子,如执行adb install 或pm install -r命令安装。但我想这并不可靠。记得之前有做过一个应用来执行linux命令,是通过RunTime来执行命令的。
后来发现其实并不靠谱,还不如直接用C代码来实现。
下面这种调用系统隐藏api接口来实现静默安装卸载,是比较大众靠谱的,实现自定义的提示界面。O(∩_∩)O~
隐藏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种方式实现:
1.直接只取IPackageDeleteObserver.aidl和IPackagerInstallObserver.aidl、IPackageMoveObserver.aidl等要使用的接口,然后通过bindService来和系统连接服务,然后直接调用接口即可(这种没有方式作者没试过,不过原理上来说应该是可行的,除非系统没有这个Service实现这个接口。有需求的可以深究下)
2.作者此处的方法是直接拷贝了源码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编译的话,并记得签名(不会的话戳这里):
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="xxx.xxx.xxxx" android:installLocation="internalOnly" android:versionCode="1" android:versionName="1.0.19" android:sharedUserId="android.uid.system" > <uses-sdk android:minSdkVersion="4"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.DELETE_PACKAGES" /> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> ... </manifest>
注:最后特别注意一点,因为下载的apk等只有rw----- root可读写权限,必须用个办法来给下载的apk赋权限,让系统级应用可以打开操作。这里作者是通过一个jni来调用C层接口,实现给指定的apk赋权限。然后执行安装apk过程
permission_change.cpp
#include <jni.h> #include <string.h> #include <android/log.h> #include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #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)
条件:
获取系统权限
拷贝apk到data/app
注:4.2和4.0上以测试通过,不过没有回调提示的,需要自己代码实现
最最后,眼见为实:附上效果图(注ddms截图有色差,不知道咋解决,有知道的请告之,万分感谢):
遇到很多朋友实现静默安装时,来问很多各式各样的问题,这里我说明下:
我只是提供了一种我实现的方式,并不是个公共的模板,大家实现的时候肯定会遇到很多问题,有些编译不过,或者那些类找不到,参数不对等等,我也感到很无力。
这里我只是记录自己工作中实现时大的方向,并且所有相关的代码都在里面。如果有哪些模糊不明白的,欢迎大家来咨询;但是像一些编译问题我觉得大家还是自己解决,并且各个环境不一样,我也不一定能解决。
最后我只能很确切的保证,以上方式是绝对可行的。但是也有局限性,并不是所有平台都能通用的,只能是系统用户组的apk才能调用隐藏接口,并需要签名(不同平台的签名肯定是不一样的)。具体原因可以看我后面的一篇介绍签名的文章