Android卸载监听详解

目前市场上比较多的应用在用户卸载后会弹出意见反馈界面,比如360手机卫士,腾讯手机管家,应用宝等等,虽然本人不太认同其交互方式,但是在技术实现上还是可以稍微研究下的。其实要实现这个功能,最主要的就是监听到自己被卸载,然后弹出一个网页,具体思路如下:

1. fork 监听进程

虽然应用程序被卸载的时候会有系统广播,但是作为被卸载的应用,挂都挂掉了,这个广播也就没有意义了,所幸的是,我们可以通过当前进程调用fork函数去创建一个子进程来监听卸载。fork函数一次调用会返回两个值,子进程返回0,父进程返回子进程ID,出错则返回-1,函数原型:pid_t fork(void)

2. 创建监听文件

android应用是基于linux的,我们可以通过linux中的inotify机制来监听应用的卸载。inotify是linux内核用于通知用户空间文件系统变化的机制,文件的添加或卸载等事件都能够及时捕获到,要监听文件卸载一般三个步骤:

  • 创建inotify实例:int fileDescriptor = inotify_init();
  • 注册监听事件:int watchDescriptor = inotify_add_watch
    (fileDescriptor,path, IN_DELETE); 这个函数包含三个参数,分别是inotify实例,监听文件路径,以及事件掩码,在这里我们关注的是删除事件,所以用IN_DELETE;
  • 调用read函数开始监听:size_t len = read(int, void *, size_t); read函数也有三个参数,分别是inotify实例,inotify_event 结构的数组指针,以及要读取的事件的总长度。

关于inotify这部分的内容,可以参考这篇博客:
http://blog.csdn.net/myarrow/article/details/7096460

3. 打开网页

打开网页很简单,直接调用execlp("am", "am", "start", "--user", userSerialNumber, "-a","android.intent.action.VIEW", "-d", url, (char *) NULL);唯一要注意的是userSerialNumber,android API 17 引入了多用户支持,所以需要userSerialNumber来标识用户。获取userSerialNumber方法如下:

private String getUserSerial(Context context) {  
    Object userManager = context.getSystemService("user");  
    if (userManager == null) {  
        return null;  
    }  
    try {  
        Method myUserHandleMethod = android.os.Process.class.getMethod(  
        "myUserHandle", (Class<?>[]) null);  
        Object myUserHandle = myUserHandleMethod.invoke(  
        android.os.Process.class, (Object[]) null);  

        Method getSerialNumberForUser = userManager.getClass().getMethod(  
            "getSerialNumberForUser", myUserHandle.getClass());  
        long userSerial = (Long) getSerialNumberForUser.invoke(userManager, myUserHandle);  
        return String.valueOf(userSerial);  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    return null;  
}  

以上内容基本解决了卸载监听的问题,但这肯定是不够的,还有很多细节需要考虑,先上代码,再来慢慢分析:

JNIEXPORT int JNICALL Java_com_uninstall_browser_sdk_UninstallBrowserSDK_init(  
    JNIEnv * env, jobject thiz, jstring arg0, jstring arg1, jstring userSerial) {  

    const char *pkgName = (*env)->GetStringUTFChars(env, arg0, 0);  
    const char *url = (*env)->GetStringUTFChars(env, arg1, 0);  

    __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "init jni");  

    // fork子进程,以执行轮询任务  
    pid_t pid = fork();  
    if (pid < 0) {  
        __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "fork failed");  
    } else if (pid == 0) {  
        // 子进程注册目录监听器  
        int fileDescriptor = inotify_init();  
        if (fileDescriptor < 0) {  
            __android_log_print(ANDROID_LOG_INFO, "JNIMsg",  "inotify_init failed");  
            exit(1);  
        }  

        int watchDescriptor;  
        watchDescriptor = inotify_add_watch(fileDescriptor, get_watch_file(pkgName), IN_DELETE);  
        if (watchDescriptor < 0) {  
            __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "inotify_add_watch failed");  
            exit(1);  
        }  
        // 分配缓存,以便读取event,缓存大小等于一个struct inotify_event的大小,这样一次处理一个event  
        void *p_buf = malloc(sizeof(struct inotify_event));  
        if (p_buf == NULL) {  
            __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "malloc failed");  
            exit(1);  
        }  
        // 开始监听  
        __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "start observer");  
        while (1) {  
            size_t readBytes = read(fileDescriptor, p_buf, sizeof(struct inotify_event));  
            // read会阻塞进程,走到这里说明收到监听文件被删除的事件,但监听文件被删除,可能是卸载了软件,也可能是清除了数据  
            FILE *p_appDir = fopen(pkgName, "r");  
            // 已经卸载  
            if (p_appDir == NULL) {  
                __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "uninstalled");  
                inotify_rm_watch(fileDescriptor, watchDescriptor);  
                break;  
            }  
            // 未卸载,可能用户执行了"清除数据",重新监听  
            else {  
                __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "clean data");  
                fclose(p_appDir);  
                int watchDescriptor = inotify_add_watch(fileDescriptor, get_watch_file(pkgName), IN_DELETE);  
                if (watchDescriptor < 0) {  
                    __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "inotify_add_watch failed");  
                    free(p_buf);  
                    exit(1);  
                }  
            }  
        }  

        free(p_buf);  
        if (userSerial == NULL) {  
            // 执行命令am start -a android.intent.action.VIEW -d $(url)  
            execlp("am", "am", "start", "-a", "android.intent.action.VIEW", "-d", url, (char *) NULL);  
        } else {  
            // 执行命令am start --user userSerial -a android.intent.action.VIEW -d $(url)  
            const char *userSerialNumber = (*env)->GetStringUTFChars(env, userSerial, 0);  
            execlp("am", "am", "start", "--user", userSerialNumber, "-a", "android.intent.action.VIEW", "-d", url, (char *) NULL);  
            (*env)->ReleaseStringUTFChars(env, userSerial, userSerialNumber);  
        }  
        execlp("am", "am", "start", "--user", "0", "-a", "android.intent.action.VIEW", "-d", url, (char *) NULL);  
        (*env)->ReleaseStringUTFChars(env, arg0, pkgName);  
        (*env)->ReleaseStringUTFChars(env, arg1, url);  
    } else {  
        (*env)->ReleaseStringUTFChars(env, arg0, pkgName);  
        (*env)->ReleaseStringUTFChars(env, arg1, url);  
        return pid;  
    }  
    return -1;  
}

问题一:监听哪个文件?

其实这个问题在于,如何判断应用是被卸载,还是覆盖安装或只是清除了数据,很显然,如果是监听应用所在目录,那当应用被覆盖安装时,马上就会监听到卸载事件,弹出网页,这个情况肯定是需要避免的。我们知道,应用程序被覆盖安装时,数据文件是不会被删掉的,那是否就可以监听这个目录?当然也是不行的,因为一旦用户执行了清除数据操作,也会弹出网页。所以,最好的办法是自己创建一个监听文件,当用户清除数据时,判断应用所在目录存不存在,若存在则说明是清除数据操作,然后重新监听,如果用户是覆盖安装,则不会触发此监听事件。

/**  
* 创建监听文件,避免覆盖安装被判断为卸载事件
*/  
char* get_watch_file(const char* package) {  
    int len = strlen(package) + strlen("watch.tmp") + 1;  
    char* watchPath = (char*) malloc(sizeof(char) * len);  
    sprintf(watchPath, "%s/%s", package, "watch.tmp");  
    FILE* file = fopen(watchPath, "r");  
    if (file == NULL) {  
        file = fopen(watchPath, "w+");  
        chmod(watchPath, 0755);  
    }  
    fclose(file);  
    __android_log_print(ANDROID_LOG_INFO, "JNIMsg", "创建文件目录 : %s", watchPath);  
    return watchPath;  
}

问题二:如何判断监听进程是否存在?

要实现监听功能,我们必须在合适的时间点去创建监听进程,一般可以选在应用第一次开启以及监听到开机广播的时候,那么问题来了,如果用户每次打开软件的时候都去创建监听进程,这显然是不科学的,所以我们应该在创建进程前先判断该监听进程是否存在,如果不存在才创建:

/**  
* 设置软件卸载时弹出网页的URL  
*/  
public void setUninstallWebUrl(Context context, String url) {  
    if (url == null || url.length() == 0) {  
        return;  
    }
    int mMonitorPid = ConfigDao.getInstance(context).getMonitorPid();  
    if (mMonitorPid > 0 && !getNameByPid(mMonitorPid).equals("!")) {  
        Log.i("stefanli", "监控进程存在");  
        return;  
    } else {  
        int mPid = init("/data/data/" + context.getPackageName(), url, getUserSerial(context));  
        Log.i("stefanli", "监控进程ID:" + mPid);  
        Log.i("stefanli", "监控进程名称:" + getNameByPid(mPid));  
        ConfigDao.getInstance(context).setMonitorPid(mPid);  
    }  
}
JNIEXPORT jstring JNICALL Java_com_uninstall_browser_sdk_UninstallBrowserSDK_getNameByPid(  
    JNIEnv * env, jobject thiz, jint pid) {  
    char task_name[100];  
    getPidName(pid, task_name);
    jsize len = strlen(task_name);  
    jclass clsstring = (*env)->FindClass(env, "java/lang/String");  
    jstring strencode = (*env)->NewStringUTF(env, "GB2312");  
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "<init>", "([BLjava/lang/String;)V");  
    jbyteArray barr = (*env)->NewByteArray(env, len);  
    (*env)->SetByteArrayRegion(env, barr, 0, len, (jbyte*) task_name);  
    return (jstring) (*env)->NewObject(env, clsstring, mid, barr, strencode);  
}  

void getPidName(pid_t pid, char *task_name) {  
    char proc_pid_path[BUF_SIZE];  
    char buf[BUF_SIZE];  
    sprintf(proc_pid_path, "/proc/%d/status", pid);  
    FILE* fp = fopen(proc_pid_path, "r");  
    if (NULL != fp) {  
    if (fgets(buf, BUF_SIZE - 1, fp) == NULL) {  
        fclose(fp);  
    }  
    fclose(fp);  
    sscanf(buf, "%*s %s", task_name);  
    } 
}

Demo下载地址:http://download.csdn.net/detail/a378881925/8373409

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 原创链接 (http://blog.csdn.net/stefanli1991/article/details/4...
    yoosir阅读 4,733评论 1 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 番茄工作法第四篇|内容总结 A.番茄工作法是什么? 番茄工作法是简单易行的时间管理方法,是由弗朗西斯科•西里洛于1...
    小葱铁憨憨阅读 212评论 2 0
  • 每次看完一本书,总会沉思很久,思绪漂浮,要经过很久的思索才会慢慢把那份不定的思绪沉淀下来,渐渐有了重量感。 过去了...
    倾耳倾听16阅读 372评论 2 1