一个应用如何检测到自己将被卸载?

94

我们都知道通常(实际上任何)杀毒软件在卸载之前会触发一个简单的对话框,类似于:“您要卸载该应用程序,确定吗?” - “是/否”。

是的,我知道我可以使用意图过滤器拦截软件包删除意图,例如:

<activity
    android:name=".UninstallIntentActivity"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <action android:name="android.intent.action.DELETE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="package"  />
    </intent-filter>
</activity>

问题在于它截获了任何删除请求,并在我的应用程序和系统安装程序之间触发选择器对话框。因此,如果用户选择系统安装程序,我将无能为力。

我的目标不是防止用户卸载我的应用程序,而只是回滚应用程序所做的更改。

从那些杀毒软件应用中学习,我看到这种操作是可能的,请帮助我并解释它是如何实现的?

更新

由于有一些人不相信这是真的 - 我想提到Avast Mobile Security

Anti-Theft通过各种自我保护技术伪装其组件来保护自己免受卸载。

另一个例子:Kaspersky Internet Security for Android - 这是卸载它的特殊过程,需要输入秘密代码。

无论如何,这意味着有方法可以拦截卸载过程,以防止卸载或完成某些最终工作。


您的应用程序进行了哪些更改,需要回滚? - Nachi
1
这些应用程序监视logcat,并在检测到您尝试卸载应用程序时要求您输入PIN码。它们通过在正确的时间启动活动来询问您:此活动将自然地出现在您和默认安装之间。 - Sherif elKhatib
顺便提一下:我刚刚安装了“杀毒软件”应用程序,并在应用程序未询问任何问题的情况下将其卸载。API 18。 - flx
7
大多数反病毒和防盗应用程序会将自己安装为设备管理员。请参考此链接- http://developer.android.com/guide/topics/admin/device-admin.html。 - Varun
有些 Avast 的功能需要它作为管理员(或根用户)才能使用。其中之一就是远程擦除。 - Jon
显示剩余6条评论
4个回答

167

好的。我已经花了两天时间调查这个问题,最终找到了一种"狂野的方法"来解决它,而不需要对设备进行root :)

首先,以下是解决方案的要点:

1. 每当用户进入设置 -> 应用管理 -> 选择特定应用程序时, 我们会收到一个广播android.intent.action.QUERY_PACKAGE_RESTART,广播中包含应用程序包的名称作为参数。

2. 在此之后,当我们点击卸载按钮(使用包安装程序)时,它会打开一个名为com.android.packageinstaller.UninstallerActivity的活动

控制流程如下:

在应用程序设置下,用户点击卸载按钮 ---> 我们控制显示对话框/启动另一个活动/等等 ---> 我们完成预卸载任务 ---> 用户返回到卸载确认屏幕 ---> 用户确认并卸载应用程序。

使用的方法:

我们将在我们的应用程序中实现BroadcastReceiver,以监听动作"android.intent.action.QUERY_PACKAGE_RESTART",并在onReceive()方法中匹配我们的软件包名称。如果广播是为选择我们所需的应用程序包而接收到的,则我们将启动一个后台线程,该线程将使用ActivityManager监视前台运行的活动。

一旦我们发现前台活动是"com.android.packageinstaller.UninstallerActivity",就会确认用户想要卸载我们的应用程序。此时,我们将执行所需的任务(显示对话框或启动另一个在卸载窗口上重叠的活动等),然后允许用户继续确认卸载进程。

实现/源代码:

在manifest.xml中

添加权限:

<uses-permission android:name="android.permission.GET_TASKS"/>

和广播接收器:

<receiver android:name=".UninstallIntentReceiver">
      <intent-filter android:priority="0">
            <action android:name="android.intent.action.QUERY_PACKAGE_RESTART" />
            <data android:scheme="package" />
      </intent-filter>
 </receiver>

UninstallIntentReceiver.java(卸载意图接收器类)

public class UninstallIntentReceiver extends BroadcastReceiver{

    @Override
    public void onReceive(Context context, Intent intent) {
        // fetching package names from extras
        String[] packageNames = intent.getStringArrayExtra("android.intent.extra.PACKAGES"); 

        if(packageNames!=null){
            for(String packageName: packageNames){
                if(packageName!=null && packageName.equals("YOUR_APPLICATION_PACKAGE_NAME")){
                    // User has selected our application under the Manage Apps settings
                    // now initiating background thread to watch for activity
                    new ListenActivities(context).start();

                }
            }
        }
    }

}

ListenActivities类 - 用于监控前台活动

class ListenActivities extends Thread{
    boolean exit = false;
    ActivityManager am = null;
    Context context = null;

    public ListenActivities(Context con){
        context = con;
        am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    }

    public void run(){

        Looper.prepare();

        while(!exit){

             // get the info from the currently running task
             List< ActivityManager.RunningTaskInfo > taskInfo = am.getRunningTasks(MAX_PRIORITY); 

             String activityName = taskInfo.get(0).topActivity.getClassName();


             Log.d("topActivity", "CURRENT Activity ::"
                     + activityName);

             if (activityName.equals("com.android.packageinstaller.UninstallerActivity")) {
                // User has clicked on the Uninstall button under the Manage Apps settings

                 //do whatever pre-uninstallation task you want to perform here
                 // show dialogue or start another activity or database operations etc..etc..

                // context.startActivity(new Intent(context, MyPreUninstallationMsgActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
                 exit = true;
                 Toast.makeText(context, "Done with preuninstallation tasks... Exiting Now", Toast.LENGTH_SHORT).show();
            } else if(activityName.equals("com.android.settings.ManageApplications")) {
                // back button was pressed and the user has been taken back to Manage Applications window
                          // we should close the activity monitoring now
                exit=true;
            }
        }
        Looper.loop();
    }
}

已知限制:

当用户在管理应用程序设置下点击卸载按钮时,我们将执行预卸载任务,然后提示用户确认窗口,在此窗口中,用户可以确认卸载或取消操作。

上述方法目前还未涵盖以下情况:如果用户在我们执行任务后单击取消按钮。但这可以通过一些修改轻松解决。

例如:如果最终未收到广播"android.intent.action.PACKAGE_REMOVED",则可以实现逻辑以恢复我们所做的更改。

我希望这种方法对您有所帮助:) 在我看来,这是唯一的方法,可以在不root设备的情况下解决您的问题!

[更新 1]: 检查卸载任务是否被取消的建议方法:

很有趣的是,我之前有完全不同且更为复杂的想法(涉及广播、ActivityManager等等),但在此处写作时,我脑海中突然浮现了另一个相对简单的想法 :)

当用户在管理应用程序设置下点击卸载按钮并在执行预卸载任务后,您只需要在应用程序中设置一些SharedPreference,告知您已执行预卸载任务并准备好卸载。之后您就不必再关心任何事情了。

如果用户继续卸载->这很好,因为您已经执行了所需的任务。

如果用户最终单击取消按钮并离开->不要费心。直到用户再次运行您的应用程序。现在,在应用程序的主活动的"onStart()" / "onResume()"中,您可以检查SharedPreference的值,如果它被设置为卸载,则表示用户最终没有继续执行卸载。现在,您可以撤消之前进行的更改(反转进行的预卸载任务),以确保您的应用程序运行正常!


在发布之前,我确保它能够正常运行 :) 但是由于我的Jelly Bean手机正在维修中,所以我无法在更高版本上进行测试 :( 如果您需要进一步的帮助,请告诉我。 - AnniJais
1
感谢您的慷慨奖励!这几乎是我当前总声望的1.5倍,对我来说意义重大 :) 关于您的两个问题-- 1)您可以创建一个活动并像我代码中注释的那样启动它。在此活动中,您可以警告/重新询问用户以确保..因此,您可以将他转发到主屏幕(例如最小化窗口的情况,这将隐藏卸载屏幕),或者也许有一种方法使用ActivityManager实际关闭原始包卸载窗口。我没有尝试过,但您可以探索一下。 - AnniJais
3
<uses-permission android:name="android.permission.GET_TASKS"/> 该权限已在API级别21被废弃 - Durai Amuthan.H
2
只是想说感谢你在更新1中提出的绝妙想法! - Nathan Adams
4
这个过程还能用吗?有人在最新的API级别上尝试过吗? - Farwa
显示剩余22条评论

10

当我在4.4中运行整个项目时,没有日志显示。您能告诉我用户点击卸载按钮时如何查看日志吗? - Renu Thakur
1
请注意,自Android 5.0起,此方法已不再适用:https://github.com/pelotasplus/ActionAfterUninstall/issues/1 - Sam

4

如果您修改内核,那么可能是可以的。在 data/data/your.app.package 中创建的所有文件都会在安装时自动删除。

另一种方法可能是有另一个应用程序检查是否已安装此应用程序。如果没有,它可以进行清理工作。

更新

ACTION_PACKAGE_REMOVED 意图将发送给除自己以外的所有接收器。这已经被确认 HERE

更新2

只是另一个想法。

当我在搜索时发现,可以通过监视 logcat 来完成此操作here 是一个示例 logcat 监视器

好消息是,要监视同一应用程序的 logcat,我们不需要 root 设备

并且当我们阅读 logcat 中的每个条目时,我们可以搜索以下字符串

Received broadcast Intent { act=android.intent.action.PACKAGE_REMOVED dat=package:com.package.name flg=0x8000010 (has extras) }

一旦收到这个事件,我们就知道我们的应用程序现在将被卸载。

虽然没有尝试过。

同样,从Android Jellybean开始不允许监视logcat。


9
请执行以下操作:
  1. 在您的设备上安装Avast杀毒软件应用程序。
  2. 尝试卸载它。
  3. 您会看到Avast会弹出对话框询问“您确定吗?”
P.S. 这在任何设备上都会发生(我指未root的设备)。然后您是在说这是不可能的?!
- Barmaley
1
给答案点赞和给问题点踩……再加上悬赏也无法改变现实。 - Chris Stratton
4
你应该阅读评论串。作者举了一个市场上的应用程序(Avast)作为例子,它成功地使其工作,所以现实可能比你想象的更加复杂。 - yoah
1
你这么说很有趣,因为你在之前的评论中指出了发帖者引用的应用程序并没有做到他所声称的那样... - Chris Stratton
要在Android 4.2中读取日志,您必须具有root权限。您可以尝试以下方法:https://dev59.com/cmgu5IYBdhLWcg3wQE4D#12642918,然后搜索logcat以查找:`Starting activity: Intent { act=android.intent.action.DELETE dat=package:your.package cmp=com.android.packageinstaller/.UninstallerActivity }`。 - Andres
显示剩余8条评论

1
为使您的应用程序持久存在,您需要拥有已取得 root 权限的设备并能够将其安装到系统分区中。一旦安装完成,您可以卸载更新,因为它们与非系统应用程序一起保存,但是从系统中卸载它并不那么简单。
我知道其中一些应用程序还会在系统分区上保存一些数据,以防设备恢复出厂设置,但也有办法让包管理器在仅卸载时保留您的保存数据。
另一个选择是将其注册为设备管理员。一旦这样做,除非他们手动删除其管理员状态,否则他们将无法卸载它。

enter image description here

<item name="android.permission.ACCESS_SUPERUSER" />

这里看起来他们使用了root以及其他方法。除非制作一些疯狂复杂的服务,否则没有其他合法的方法可以做到这一点,而它似乎已经实现了。利用root几乎是像这样的AV/安全应用程序的标准做法,没有它,它们对其他应用程序没有任何真正的控制权,因此它们非常有限。我认为SuperUser权限除非你已经安装了它,否则不会显示,因此许多人仍然不知道它是一个选项。
<perms>
<item name="android.permission.READ_EXTERNAL_STORAGE" />
<item name="android.permission.GET_TASKS" />
<item name="android.permission.PROCESS_OUTGOING_CALLS" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" />
<item name="android.permission.WRITE_CALL_LOG" />
<item name="com.avast.android.generic.CENTRAL_SERVICE_PERMISSION" />
<item name="android.permission.WRITE_SMS" />
<item name="android.permission.ACCESS_WIFI_STATE" />
<item name="android.permission.RECEIVE_SMS" />
<item name="android.permission.GET_ACCOUNTS" />
<item name="android.permission.READ_CONTACTS" />
<item name="android.permission.CALL_PHONE" />
<item name="android.permission.WRITE_CONTACTS" />
<item name="android.permission.READ_PHONE_STATE" />
<item name="android.permission.READ_SMS" />
<item name="android.permission.RECEIVE_BOOT_COMPLETED" />
<item name="android.permission.ACCESS_SUPERUSER" />
<item name="com.avast.android.mobilesecurity.permission.C2D_MESSAGE" />
<item name="android.permission.GET_PACKAGE_SIZE" />
<item name="android.permission.WAKE_LOCK" />
<item name="android.permission.ACCESS_NETWORK_STATE" />
<item name="android.permission.USE_CREDENTIALS" />
<item name="android.permission.SEND_SMS" />
<item name="android.permission.RECEIVE_MMS" />
<item name="com.google.android.c2dm.permission.RECEIVE" />
<item name="android.permission.KILL_BACKGROUND_PROCESSES" />
<item name="com.android.vending.BILLING" />
<item name="android.permission.WRITE_SETTINGS" />
<item name="android.permission.INTERNET" />
<item name="android.permission.VIBRATE" />
<item name="android.permission.READ_CALL_LOG" />
<item name="com.avast.android.generic.COMM_PERMISSION" />
<item name="com.dolphin.browser.permission.ACCESS_PROVIDER" />
<item name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
</perms>

4
正如原帖和评论所指出的,Avast Mobile Security可以在不获取Root权限的情况下实现此功能。在未获取Root权限的4.2.2 Galaxy Nexus上进行过测试并得到验证。 - Geobits
1
有很多方法可以解决这个问题,只取决于你愿意投入多少精力以及你想要它有多彻底。 - Jon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接