Android 如何实现应用卸载反馈,卸载监控

    市面上有很多Android 的App在卸载之后会弹出一个反馈页面,让用户填写卸载原因,收集用户的卸载反馈。这是怎么实现的呢?应用自身已经被卸载了,怎么还能弹出一个反馈页面呢 ?首先,可以排除的是BroadCastReceiver,因为应用已经被卸载了,BroadCastReceiver是不可能有机会接收到卸载消息的。所以肯定是有一个后台在监控,那么会是android service吗?肯定不可能,service跟BroadCastReceiver一样当应用卸载的时候会被系统干掉!

因此后台监控应该具有如下特点:

  • 监控程序应该由App第一次安装启动时,创建触发;并且能监控到App什么时候被卸载了
  • App被卸载的时候,监控程序并不受影响,仍然能在后台运行,并且能感知到此时App被卸载了

要具备以上两个条件,监控程序必须是在一个独立的进程中运行:

  • 这个独立的进程肯定是App的一个子进程,并且在App第一次安装启动时创建的
  • 当App卸载后这个子进程仍能够在后台运行

因此监控程序只能是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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/inotify.h>
#include <android/log.h>
#include <fcntl.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <dlfcn.h>

//该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 <ul>
     *         <li>if isNeedResultMsg is false, {@link CommandResult#successMsg}
     *         is null and {@link CommandResult#errorMsg} is null.</li>
     *         <li>if {@link CommandResult#result} is -1, there maybe some
     *         excepiton.</li>
     *         </ul>
     */
    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
     * <ul>
     * <li>{@link CommandResult#result} means result of command, 0 means normal,
     * else means error, same to excute in linux shell</li>
     * <li>{@link CommandResult#successMsg} means success message of command
     * result</li>
     * <li>{@link CommandResult#errorMsg} means error message of command result</li>
     * </ul>
     * 
     * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a>
     *         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;
        }
    }

}

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值