目前市場上比較多的應用在用戶卸載后會彈出意見反饋界面,比如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);
}
}