May 29, 2016

Monitor Application Removal on Android

If an application is removed in PC, a web page will pop up usually. This make developers get feedback from users. Recently I've been working on how to get the same feature on Android.

Plan A

At first, I think it's a pretty simple task. I remember that there's a broadcast of action ACTION_PACKAGE_REMOVED when application is uninstalled. So we listen to this broadcast and do the work on onReceive() method? Then I wrote a simple POC and proved that it's totally infeasible. The broadcast is sent by AMS after the application removed from device. The code in our listener never gets a chance to execute. So it's not possible? Fortunately, we have Plan B.

Plan B

As Android will remove the data folder of application after uninstallation, if the data folder is removed, we could know our application has been removed. So are we gonna do the polled operation to determine the status of data folder? Absolutely not, Linux provided a mechanism called inotify, with the inotify api, we could get notified when operations have been performed on the file or directory. Generally, we have to do the following steps to use the inotify api:

  • Create an inotify instance with inotify_init()
  • Add file or directory to watch list of inotify instance with inotify_add_watch()
  • read() from the inotify file descriptor to wait for the event
  • Remove file or directory from watch list with inotify_rm_watch()

Now here comes the problem. How do we make the monitor process running even after the application has been removed? I've made an experiment and found that the native process forked will survive after the uninstallation. I do the following things to get all the work done:

  • Start an empty service on attachBaseContext() method of Application.
  • Call native method on onStartCommand() method of service.
  • Fork a native process in native method and monitor the data folder with inotify api.
  • Kill the parent process of native monitor process, so that native monitor process became orphan process and adopted by init process.
  • Do the stuff after getting the IN_DELETE_SELF event from inotify file descriptor.

Here's the JNI code:

#include <errno.h>
#include <limits.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <android/log.h>
#include <sys/inotify.h>
#include "guard.h"

static const char* class_path_name = "com/shunix/uninstallguard/service/GuardService";
static const char* data_path = "/data/data/";
static JNINativeMethod methods[] = {
    {"startGuard", "(Ljava/lang/String;)V", &StartGuard}
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    LOGD("JNI_OnLoad");
    JNIEnv* env;
    int ret = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
    if (ret == JNI_EDETACHED) {
        LOGD("JNI_EDETACHED");
        if ((*vm)->AttachCurrentThread(vm, &env, NULL) != 0) {
            LOGD("AttachCurrentThread Error");
            return JNI_ERR;
        }
    } else if (ret == JNI_OK) {
        jclass clazz = (*env)->FindClass(env, class_path_name);
        (*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0]));
        return JNI_VERSION_1_6;
    } else {
        LOGD("JNI_OnLoad Error");
        return JNI_ERR;
    }
}

JNIEXPORT void JNICALL StartGuard(JNIEnv* env, jobject obj, jstring package_name) {
    LOGD("StartGuard");
    const char* native_package_name = (*env)->GetStringUTFChars(env, package_name, NULL);
    LOGD("Package Name: %s", native_package_name);
    char* dest_path = (char*) calloc(BUFFER_LEN, sizeof(char));
    strcpy(dest_path, data_path);
    strcat(dest_path, native_package_name);
    LOGD("Dest Path: %s", dest_path);
    pid_t pid = fork();
    if (pid < 0) {
        LOGD("Unable to fork");
    } else {
        if (pid == 0) {
            int inotify_fd = inotify_init();
            int watch_des = inotify_add_watch(inotify_fd, dest_path, IN_DELETE_SELF);
            if (watch_des == -1) {
                return;
            } else {
                LOGD("Watch Descriptor: %d", watch_des);
            }
            uint8_t buffer[INOTIFY_BUFFER_LENGTH];
            for(;;) {
                fd_set fds;
                FD_ZERO(&fds);
                FD_SET(inotify_fd, &fds);
                int ret = select(FD_SETSIZE, &fds, NULL, NULL, NULL);
                if (ret > 0 && errno != EINTR) {
                    LOGD("select returned %d", ret);
                    int read_length = read(inotify_fd, buffer, INOTIFY_BUFFER_LENGTH);
                    uint8_t* p = buffer;
                    if (read_length > 0) {
                        for (int i  = 0; i < read_length;) {
                            LOGD("App was uninstalled");
                            int name_length = ((struct inotify_event*) p)->len;
                            p += (sizeof(struct inotify_event) + name_length);
                            i += (sizeof(struct inotify_event) + name_length);
                        }
                    }
                }
            }
        } else {
            LOGD("Forked pid: %d", pid);
            exit(-1);
        }
    }
    (*env)->ReleaseStringUTFChars(env, package_name, native_package_name);
}

There're several things to notice:

  • This code do not work with Android Lollipop and above. As Android will kill all the process in process group when removing applications, native monitor process won't survive.
  • Before we fork the native monitor process, we need to check if the process is already running. I achieve this by simply checking the process name in service.
  • The select is not required here, simply read from inotify file descriptor is OK.

Full code can be found on my github, UninstallGuard.