Android 解除安裝監聽詳解

奇巧巧克力發表於2015-01-18

        目前市場上比較多的應用在使用者解除安裝後會彈出意見反饋介面,比如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
        






相關文章