0%

Android-卸载反馈

1. 前言

PC时代,把某些软件卸载后会弹出一个网页,用来收集反馈信息,以便产品更好决策。在移动时代,Android其实也可以做到同样的功能。

2. 分析

当用户卸载app时,Android系统会自动杀掉app所在进程,在framework层,并没有提供app卸载监听方法。我们知道Android是基于Linux的,Linux内核提供了许多进程管理的方法,通过fork函数我们可以轻松创建一个子进程,结合Java语言的JNI机制,就可以在app启动时创建一个子进程实时监测当前app的包名是否被删除,当包名被删除时,通过命令打开网页即可。

3. 实现

先通过JNI调用,fork一个子进程实时监控包名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
JNIEXPORT void JNICALL
Java_com_netease_uninstallmonitor_MainActivity_monitorUsingPoll(JNIEnv *env, jobject instance,
jstring pkgDir_, jint sdkVersion_,
jstring openUrl_) {
const char *pkgDir = env->GetStringUTFChars(pkgDir_, 0);
const char *openUrl = env->GetStringUTFChars(openUrl_, 0);
// need to clear old process
clearOldProcessByName(monitorProcessName);
int state = fork();
if (state > 0) {
LOGD("parent process = %d", state);
} else if (state == 0) {
LOGD("child process = %d", state);
int stop = 0;
while (!stop) {
sleep(1);
FILE *file = fopen(pkgDir, "r");
if (file == NULL) {
LOGD("app uninstalled already!");
loadUrl(sdkVersion_, openUrl);
stop = 1;
}
}
} else {
LOGD("Error");
}
env->ReleaseStringUTFChars(pkgDir_, pkgDir);
env->ReleaseStringUTFChars(openUrl_, openUrl);
}

当包名被删除时打开url即可

1
2
3
4
5
6
7
8
9
10
void loadUrl(int sdkVersion, const char *url) {
LOGD("start open url....");
if (sdkVersion < 17) {
execlp("am", "am", "start", "-a", "android.intent.action.VIEW", "-d", url, NULL);
} else {
execlp("am", "am", "start", "--user", "0", "-a",
"android.intent.action.VIEW", "-d", url, (char *) NULL);
}
LOGD("stop open url....");
}

4. 优化

上述方案使用while语句一直循环监听,浪费资源,我们可以基于linux的文件删除通知机制来进行优化。

优化点一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
JNIEXPORT void JNICALL
Java_com_netease_uninstallmonitor_MainActivity_monitorUsingNotify(JNIEnv *env, jobject instance,
jstring pkgDir_, jint sdkVersion,
jstring openUrl_) {
const char *pkgDir = env->GetStringUTFChars(pkgDir_, 0);
const char *openUrl = env->GetStringUTFChars(openUrl_, 0);
int pid = fork();
if (pid > 0) {
LOGD("parent process = %d", pid);
} else if (pid == 0) {
LOGD("child process = %d", pid);
prctl(PR_SET_NAME, monitorProcessName);
// register notify listener
int fd = inotify_init();
if (fd < 0) {
LOGD("inofity_init failed!!!");
exit(1);
}
int wd = inotify_add_watch(fd, pkgDir, IN_DELETE);
if (wd < 0) {
LOGD("inotify_add_watch failed !!!");
exit(1);
}
// handle an event once a time
void *p_buf = malloc(sizeof(inotify_event));
if (p_buf == NULL) {
LOGD("malloc failed!!!");
exit(1);
}
LOGD("start listen");
// block session until file delete
read(fd, p_buf, sizeof(struct inotify_event));
free(p_buf);
inotify_rm_watch(fd, IN_DELETE);
loadUrl(sdkVersion, openUrl);
LOGD("end listen");
} else {
LOGD("Error");
}
env->ReleaseStringUTFChars(pkgDir_, pkgDir);
env->ReleaseStringUTFChars(openUrl_, openUrl);
}

优化点二:

上述放方案中,当app卸载重装后,之前的监听进程可能没有被系统杀死,因此可能导致系统中可能同时存在多个监听进程,浪费资源。因此在创建监听进程前最好把之前的监听进程杀死。

5. 最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <stdio.h>
#include <jni.h>
#include <malloc.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/inotify.h>
#include <sys/prctl.h> /*for prctl function*/
#include <dirent.h> /*for DIR struct*/
#include <android/log.h>
#define LOG_TAG "uninstall_tag"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C"
{
char monitorProcessName[] = "ntes_uninstall";
void loadUrl(int sdkVersion, const char *url) {
LOGD("start open url....");
if (sdkVersion < 17) {
execlp("am", "am", "start", "-a", "android.intent.action.VIEW", "-d", url, NULL);
} else {
execlp("am", "am", "start", "--user", "0", "-a",
"android.intent.action.VIEW", "-d", url, (char *) NULL);
}
LOGD("stop open url....");
}
int find_pid_by_name(const char *ProcName, int *foundpid) {
DIR *dir;
struct dirent *d;
int pid, i;
char *s;
int pnlen;
i = 0;
foundpid[0] = 0;
pnlen = strlen(ProcName);
/* Open the /proc directory. */
dir = opendir("/proc");
if (!dir) {
LOGE("cannot open /proc");
return -1;
}
/* Walk through the directory. */
while ((d = readdir(dir)) != NULL) {
char exe[PATH_MAX + 1];
char path[PATH_MAX + 1];
int len;
int namelen;
/* See if this is a process */
if ((pid = atoi(d->d_name)) == 0) continue;
snprintf(exe, sizeof(exe), "/proc/%s/exe", d->d_name);
if ((len = readlink(exe, path, PATH_MAX)) < 0)
continue;
path[len] = '\0';
/* Find ProcName */
s = strrchr(path, '/');
if (s == NULL) continue;
s++;
/* we don't need small name len */
namelen = strlen(s);
if (namelen < pnlen) continue;
if (!strncmp(ProcName, s, pnlen)) {
/* to avoid subname like search proc tao but proc taolinke matched */
if (s[pnlen] == ' ' || s[pnlen] == '\0') {
foundpid[i] = pid;
i++;
}
}
}
foundpid[i] = 0;
closedir(dir);
return 0;
}
void clearOldProcessByName(const char *monitorProcessName) {
int i, killFailed, findResult, oldPids[128];
findResult = find_pid_by_name(monitorProcessName, oldPids);
if (!findResult) {
if (oldPids[0] == 0)
LOGD("have no old process!");
for (i = 0; oldPids[i] != 0; i++) {
LOGD("old process pid=%d ", oldPids[i]);
// return zero if kill process successfully
killFailed = kill(oldPids[i], SIGKILL);
if (killFailed) {
LOGD("kill old process pid=%d failed!", oldPids[i]);
} else {
LOGD("kill old process pid=%d success!", oldPids[i]);
}
}
} else {
LOGE("clear old process failed!");
}
}
JNIEXPORT void JNICALL
Java_com_netease_uninstallmonitor_MainActivity_monitorUsingPoll(JNIEnv *env, jobject instance,
jstring pkgDir_, jint sdkVersion_,
jstring openUrl_) {
const char *pkgDir = env->GetStringUTFChars(pkgDir_, 0);
const char *openUrl = env->GetStringUTFChars(openUrl_, 0);
// need to clear old process
clearOldProcessByName(monitorProcessName);
int state = fork();
if (state > 0) {
LOGD("parent process = %d", state);
} else if (state == 0) {
LOGD("child process = %d", state);
int stop = 0;
while (!stop) {
sleep(1);
FILE *file = fopen(pkgDir, "r");
if (file == NULL) {
LOGD("app uninstalled already!");
loadUrl(sdkVersion_, openUrl);
stop = 1;
}
}
} else {
LOGD("Error");
}
env->ReleaseStringUTFChars(pkgDir_, pkgDir);
env->ReleaseStringUTFChars(openUrl_, openUrl);
}
JNIEXPORT void JNICALL
Java_com_netease_uninstallmonitor_MainActivity_monitorUsingNotify(JNIEnv *env, jobject instance,
jstring pkgDir_, jint sdkVersion,
jstring openUrl_) {
const char *pkgDir = env->GetStringUTFChars(pkgDir_, 0);
const char *openUrl = env->GetStringUTFChars(openUrl_, 0);
// need to clear old process
clearOldProcessByName(monitorProcessName);
int pid = fork();
if (pid > 0) {
LOGD("parent process = %d", pid);
} else if (pid == 0) {
LOGD("child process = %d", pid);
prctl(PR_SET_NAME, monitorProcessName);
// register notify listener
int fd = inotify_init();
if (fd < 0) {
LOGD("inofity_init failed!!!");
exit(1);
}
int wd = inotify_add_watch(fd, pkgDir, IN_DELETE);
if (wd < 0) {
LOGD("inotify_add_watch failed !!!");
exit(1);
}
// handle an event once a time
void *p_buf = malloc(sizeof(inotify_event));
if (p_buf == NULL) {
LOGD("malloc failed!!!");
exit(1);
}
LOGD("start listen");
// block session until file delete
read(fd, p_buf, sizeof(struct inotify_event));
free(p_buf);
inotify_rm_watch(fd, IN_DELETE);
loadUrl(sdkVersion, openUrl);
LOGD("end listen");
} else {
LOGD("Error");
}
env->ReleaseStringUTFChars(pkgDir_, pkgDir);
env->ReleaseStringUTFChars(openUrl_, openUrl);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.netease.uninstallmonitor;
import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText("hello world!");
String pkgDir = "/data/data/" + getPackageName();
monitorUsingNotify(pkgDir, Build.VERSION.SDK_INT, "https://www.google.com");
}
public native void monitorUsingPoll(String pkgDir, int sdkVersion, String openUrl);
/**
* recommend using this way
*
* @param pkgDir
* @param sdkVersion
* @param openUrl
*/
public native void monitorUsingNotify(String pkgDir, int sdkVersion, String openUrl);
static {
System.loadLibrary("uninstall_feedback");
}
}

6. 缺陷

Android 5.0之后,对后台进程的限制更加严格。当应用卸载后,和应用同在一个用户组的进程也会被杀掉,这意味着通过fork函数创建出来的子进程在app卸载时,也会同时被系统杀死,导致方法失效。

7. 源码

UninstallMonitor

8. 参考

android卸载反馈实现

Android卸载程序之后跳转到指定的反馈页面