您的位置:首页 > 移动开发 > Android开发

Android卸载监听详解

2015-10-29 17:16 501 查看
目前市场上比较多的应用在用户卸载后会弹出意见反馈界面,比如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方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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;
}

以上内容基本解决了卸载监听的问题,但这肯定是不够的,还有很多细节需要考虑,先上代码,再来慢慢分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

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;
}


问题一:监听哪个文件?

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

/**
* 创建监听文件,避免覆盖安装被判断为卸载事件
*/
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;
}


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

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

/**
* 设置软件卸载时弹出网页的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);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2021
22
23
24
25
26

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: