android 实现静默安装、卸载(图)

摘要:

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 实现静默安装、卸载(图)_第1张图片
    201117_HF19_587911.png

    (注:此处的包名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编译的话,并记得签名


    
    
 
    
    
    
    
    
    
     
    ...


注:最后特别注意一点,因为下载的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截图有色差,不知道咋解决,有知道的请告之,万分感谢):

android 实现静默安装、卸载(图)_第2张图片
210002_7yDO_587911.png
android 实现静默安装、卸载(图)_第3张图片
210014_nxr5_587911.png
android 实现静默安装、卸载(图)_第4张图片
210034_xR1U_587911.png
android 实现静默安装、卸载(图)_第5张图片
210048_TNM1_587911.png
6.补充说明

遇到很多朋友实现静默安装时,来问很多各式各样的问题,这里我说明下:

我只是提供了一种我实现的方式,并不是个公共的模板,大家实现的时候肯定会遇到很多问题,有些编译不过,或者那些类找不到,参数不对等等,我也感到很无力。

这里我只是记录自己工作中实现时大的方向,并且所有相关的代码都在里面。如果有哪些模糊不明白的,欢迎大家来咨询;但是像一些编译问题我觉得大家还是自己解决,并且各个环境不一样,我也不一定能解决。

最后我只能很确切的保证,以上方式是绝对可行的。但是也有局限性,并不是所有平台都能通用的,只能是系统用户组的apk才能调用隐藏接口,并需要签名(不同平台的签名肯定是不一样的)。具体原因可以看我后面的一篇介绍签名的文章

android 实现静默安装、卸载(图)_第6张图片
欢迎关注微信公众号:天涯工作室

你可能感兴趣的:(android 实现静默安装、卸载(图))