市面上有很多Android 的App在卸载之后会弹出一个反馈页面,让用户填写卸载原因,收集用户的卸载反馈。这是怎么实现的呢?应用自身已经被卸载了,怎么还能弹出一个反馈页面呢 ?首先,可以排除的是BroadCastReceiver,因为应用已经被卸载了,BroadCastReceiver是不可能有机会接收到卸载消息的。所以肯定是有一个后台在监控,那么会是android service吗?肯定不可能,service跟BroadCastReceiver一样当应用卸载的时候会被系统干掉!
因此后台监控应该具有如下特点:
要具备以上两个条件,监控程序必须是在一个独立的进程中运行:
因此监控程序只能是App的一个僵尸子进程:即由App创建,但是其父进程不能是App自身,否则当App卸载的时候,系统会把App及其所有的子进程都干掉!
我们立刻想到了C 代码的 fork() 函数,具体实现步骤如下:
1. App第一次安装启动时,加载一个可执行Bin文件,在Bin文件中 fork 出一个子进程,然后让父进程先退出,子进程变为僵尸进程继续在后台监控
2. 由于App被卸载的时候,其/data/data/$PkgName/ 下面的所有文件及目录都会被移除,因此可以在该目录下放一个文件,通过监控该文件是否被移除来感知App是否被卸载了。例如:/data/data/$PkgName/feedback,监控文件被移除可以用C函数:inotify_init, inotify_add_watch, inotify_rm_watch (这三个标准C函数的用法,自行脑补吧)
3. 当监控到/data/data/$PkgName/feedback 被移除后,有两种可能:(1)App的确被卸载了;(2)用户在应用管理里面,手动执行了“clear data” 操作;需要对这两个情况进行判断
4. 当判断App的确被卸载之后,可以直接在C层执行 "am” 命令弹出卸载反馈页面,例如 “am start -a android.intent.action.VIEW -d 卸载反馈的url”
5. 下面是可执行Bin文件C代码, 编译完后命名为libfeedback.so,之所以后缀为.so,是为了方便APK打包,将其放到Android工程目录libs/armeabi/libfeedback.so下面,这样当APK打包之后,安装的时候会自动释放到 "/data/data/com.test.app/lib/" 目录。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//该libfeedback.so其实是一个可执行BIN文件,之所以后缀为.so,是为了方便APK打包
//将其放到Android工程目录libs/armeabi/libfeedback.so下面,这样当APK打包之后,安装的时候会自动释放到下面目录
static const char APP_DIR[] = "/data/data/com.test.app/lib/libfeedback.so";
//在该目录下放一个文件feedback,通过监控该文件是否被移除来感知App是否被卸载了
static const char APP_OBSERVED_FILE[] = "/data/data/com.test.app/feedback";
int main(int argc, char** argv) {
//fork子进程
pid_t pid = fork();
if (pid < 0) {
exit(1);
} else if (pid == 0) {//子进程才会执行到此
// argv[0]是BIN文件自身的全路径
// 反馈页面的URL地址通过参数的方式传入
char *pstrHttpId = argv[1];
// OS的版本也以参数的方式传入
char *pstrSdkInt = argv[2];
//1. 若被监听文件不存在,创建文件
FILE *p_observedFile = fopen(APP_OBSERVED_FILE, "r");
if (p_observedFile == NULL) {
p_observedFile = fopen(APP_OBSERVED_FILE, "w");
}
fclose(p_observedFile);
//2. 修改文件权限防止卸载不干净的问题
int ret = chmod(APP_OBSERVED_FILE, 0755);
if (ret != 0) {
exit(1);
}
//3. 分配缓存,以便读取event,缓存大小等于一个struct inotify_event的大小,这样一次处理一个event
void *p_buf = malloc(sizeof(struct inotify_event));
if (p_buf == NULL) {
exit(1);
}
//4. 初始化监控
int fileDescriptor = inotify_init();
if (fileDescriptor < 0) {
free(p_buf);
exit(1);
}
//5. 添加被监听文件到监听列表,开始监听
int watchDescriptor = inotify_add_watch(fileDescriptor, APP_OBSERVED_FILE, IN_DELETE);
if (watchDescriptor < 0) {
free(p_buf);
exit(1);
}
while (1) {
//读取监控事件,read会阻塞线程
size_t readBytes = read(fileDescriptor, p_buf, sizeof(struct inotify_event));
FILE *p_appDir = fopen(APP_DIR, "r");
if (p_appDir == NULL) {
break; // App被卸载了
} else {
// 当真卸载的时候,"/data/data/com.test.app/lib/"目录肯定不存在;
// 而当用户在应用管理页面手动执行了"清除数据"操作,只会清除数据,lib目录是不会被清除的
fclose(p_appDir);
// 若被监听文件不存在,创建文件
FILE *p_observedFile = fopen(APP_OBSERVED_FILE, "r");
if (p_observedFile == NULL) {
p_observedFile = fopen(APP_OBSERVED_FILE, "w");
}
fclose(p_observedFile);
int ret = chmod(APP_OBSERVED_FILE, 0755);
if (ret != 0) {
exit(1);
}
int watchDescriptor = inotify_add_watch(fileDescriptor, APP_OBSERVED_FILE, IN_DELETE);
if (watchDescriptor < 0) {
free(p_buf);
exit(1);
}
}
}
int sdk_int = atoi(pstrSdkInt);
int retA, retB;
char returnValue[1024];
int bufL = 1024;
char strCmd[1024] = {0};
char strBuf[1024] = {0};
strBuf[bufL - 1] = 0;
//6. 启动卸载反馈页面
if (sdk_int >= 17) {
sprintf(strCmd, "am start --user 0 -a android.intent.action.VIEW -d \"%s\"", pstrHttpId);
} else {
sprintf(strCmd, "am start -a android.intent.action.VIEW -d \"%s\"", pstrHttpId);
}
FILE *fp = NULL;
chdir("/");
//popen() 函数用创建管道的方式启动一个进程, 并调用shell. command参数是一个字符串指针,
//这个字符串包含一个shell命令. 这个命令被送到/bin/sh以-c参数执行, 即由shell来执行。
fp = popen(strCmd, "r");
if (fp) {
fgets(strBuf, bufL - 1, fp);
sprintf(returnValue, "popen return value : %s", strBuf);
pclose(fp);
} else {
sprintf(returnValue, "popen return value : %s", "fp is NULL");
}
free(p_buf);
//7. 移除文件监控,子进程退出
inotify_rm_watch(fileDescriptor, IN_DELETE);
return 0;
}
}
6. Java 的调用代码如下:
package com.example.mytest;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class TestClass {
public static final String COMMAND_SH = "/system/bin/su";
public static final String COMMAND_EXIT = "exit\n";
public static final String COMMAND_LINE_END = "\n";
// Replace your app-uninstall feedback URL page
private static final String HTTP_URL = "http://xxx.xxx.com";
private static final String strLibPath = "/data/data/com.test.app/lib/";
private static final String strSoFilenamePath = "/data/data/com.test.app/lib/libfeedback.so";
private static final String strBinFilenamePath = "/data/data/com.test.app/libfeedback.so";
public void startObserverTask() {
new Thread(observeTask).start();
}
private Runnable observeTask = new Runnable() {
@Override
public void run() {
try {
//检查监控进程是否已经存在
CommandResult commandResult = execCommand("ps | grep \""+ strBinFilenamePath + "\"");
if (commandResult.errorMsg != null) {
//may be 'grep' not support.
commandResult = execCommand("ps");
}
if (commandResult.successMsg != null && commandResult.successMsg.contains(strBinFilenamePath)) {
//监控进程已经存在,无需再启动
return;
} else {
//将 strSoFilenamePath 备份到 strBinFilenamePath
execCommand("dd if=" + strSoFilenamePath + " of=" + strBinFilenamePath);
//启动 strBinFilenamePath 监控进程
execCommand(new String[] { "cd " + strLibPath, "chmod 755 " + strBinFilenamePath,
strBinFilenamePath + " " + HTTP_URL + " " + android.os.Build.VERSION.SDK_INT });
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
/**
* execute shell command
*/
public static CommandResult execCommand(String command) {
return execCommand(new String[] { command });
}
/**
* execute shell commands
*
* @param commands
* command array
* @param isRoot
* whether need to run with root
* @param isNeedResultMsg
* whether need result msg
* @return
* - if isNeedResultMsg is false, {@link CommandResult#successMsg}
* is null and {@link CommandResult#errorMsg} is null.
* - if {@link CommandResult#result} is -1, there maybe some
* excepiton.
*
*/
public static CommandResult execCommand(String[] commands) {
int result = -1;
if (commands == null || commands.length == 0) {
return new CommandResult(result, null, null);
}
Process process = null;
BufferedReader successResult = null;
BufferedReader errorResult = null;
StringBuilder successMsg = null;
StringBuilder errorMsg = null;
DataOutputStream os = null;
try {
process = Runtime.getRuntime().exec(COMMAND_SH);
os = new DataOutputStream(process.getOutputStream());
for (String command : commands) {
if (command == null) {
continue;
}
os.write(command.getBytes());
os.writeBytes(COMMAND_LINE_END);
os.flush();
}
os.writeBytes(COMMAND_EXIT);
os.flush();
result = process.waitFor();
// get command result
successMsg = new StringBuilder();
errorMsg = new StringBuilder();
successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s;
while ((s = successResult.readLine()) != null) {
successMsg.append(s);
}
while ((s = errorResult.readLine()) != null) {
errorMsg.append(s);
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
if (successResult != null) {
successResult.close();
}
if (errorResult != null) {
errorResult.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (process != null) {
process.destroy();
}
}
return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null : errorMsg.toString());
}
/**
* result of command
*
* - {@link CommandResult#result} means result of command, 0 means normal,
* else means error, same to excute in linux shell
* - {@link CommandResult#successMsg} means success message of command
* result
* - {@link CommandResult#errorMsg} means error message of command result
*
*
* @author Trinea
* 2013-5-16
*/
public static class CommandResult {
/** result of command **/
public int result;
/** success message of command result **/
public String successMsg;
/** error message of command result **/
public String errorMsg;
public CommandResult(int result) {
this.result = result;
}
public CommandResult(int result, String successMsg, String errorMsg) {
this.result = result;
this.successMsg = successMsg;
this.errorMsg = errorMsg;
}
}
}