kars_to_lake
As part of the enterprise features, Android offers various APIs for device administration. Many of these APIs are not available to "regular" apps and can only be executed from an app running in Device Owner or Profile Owner mode.

I already showed how to install apps programmatically without user interaction in my previous post. In this post I would like to show additional features related to device administration - uninstalling & disabling apps.

Uninstalling apps without user interaction might be useful if you are, for example, administering an Android kiosk device. Having the option to do this programmatically allows you to implement remote admin feature which can remove unwanted apps remotely without the need to have somebody present at the device to confirm the uninstall dialog. You can also use it as an automated device setup feature, after installing your kiosk app onto a new device.

The other option is to disable apps programmatically. Some apps are pre-installed on the system partition which is usually read-only. You normally won't be able to remove these apps without rooting. Android gives you the option to mark the application as "hidden". This makes the app disappear from launcher and app list, and it also prohibits to launch the app programmatically via an Intent. The app will still occupy space, but it will be inaccessible. This is again useful when you want to customize a single purpose device (like a kiosk) because it allows you to "remove" even the pre-installed standard apps (like the browser, email client, google play app,..) and make your device really single-purpose only.

In this post I'll show how to accomplish both of these tasks. If you want to find more information about device administration and device owner mode, I also recommend to check out my other posts.

You can find the sample code for this post on github.

Enabling Device Owner Mode

Making your app a device owner allows it to execute some privileged tasks that are not available to regular apps. I already showed how to do this in my previous posts. I'll give a quick summarization here, so that you don't need to switch back and forth between posts.

First step is to extend the DeviceAdminReceiver class

class DevAdminReceiver: DeviceAdminReceiver() {
    override fun onEnabled(context: Context?, intent: Intent?) {
        super.onEnabled(context, intent)
        Log.d(TAG, "Device Owner Enabled")
    }
}

Next you should create a device_admin.xml file inside of your res/xml folder

<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
    </uses-policies>
</device-admin>

Then declare a BroadcastReceiver component inside of you AndroidManifest.xml file

<application
        android:testOnly="true"

        ...>
    ...
    <receiver
        android:name="eu.sisik.kioskbrowser.DevAdminReceiver"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_DEVICE_ADMIN" >
        <intent-filter>
            <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
        </intent-filter>

        <meta-data
            android:name="android.app.device_admin"
            android:resource="@xml/device_admin" />
    </receiver>
...
</application>

Note that I also added the testOnly flag to the tag. If you are enabling device owner via ADB, this flag is necessary since Android 7.0.

Once you've built and installed the app to your target device, you can make it a devices owner with the following ADB command

adb shell dpm set-device-owner my.package.name/.DevAdminReceiver

Uninstalling Apps Programmatically

Before you try to perform the steps in this section, you need to make sure that your app has been configured as devices owner according to the steps in previous section.

To remove an app, I'll use the uninstall method of PackageInstaller class. This methods accepts the package name that you want to remove and an IntentSender. The IntentSender is used to deliver the result of the uninstall process to your app.

val intentSender = PendingIntent.getBroadcast(this,
    CODE_UNINSTALL_RESULT,
    Intent(ACTION_UNINSTALL_RESULT),
    0).intentSender

val pi = packageManager.packageInstaller

pi.uninstall("packagename.to.uninstall", intentSender)

You'll also need to add REQUEST_DELETE_PACKAGES permission to your AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="eu.sisik.removehideapps">

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

To receive the result of the uninstall process, I decided to use a BroadcastReceiver that will deliver the result to my activity. From the code above you can see that I passed my custom action (ACTION_UNINSTALL_RESULT) when I was creating the intentSender, so that I can listen for this action inside of my BroadcastReceiver. You have also other options to deliver the intent (e.g. in onStartCommand() inside of a Service by using PendingIntent.getService()), so feel free to adjust the code according to your requirements

...
private val receiver = object: BroadcastReceiver() {
    override fun onReceive(p0: Context?, intent: Intent?) {
        if(intent?.action == ACTION_UNINSTALL_RESULT) {
            Log.d(TAG, "requested to uninstall " + intent?.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) 
                     + "(result=" + intent?.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + ")")
        }
    }
}
...

override fun onResume() {
    super.onResume()
    registerReceiver(receiver, IntentFilter(ACTION_UNINSTALL_RESULT))
}

override fun onPause() {
    super.onPause()
    unregisterReceiver(receiver)
}

Once the uninstall process has finished, you should receive the package name and some status message as part of intent extras. Getting the PackageInstaller.EXTRA_STATUS_MESSAGE string should give you something like "DELETE_SUCCEEDED".

Note that you won't be able to remove pre-installed apps located on the read-only system partition and you also won't be able to uninstall your own app which was made a device owner.

Hiding Apps

This options allows you to kind of "remove" also the pre-installed apps located on system partition by making them hidden and inaccessible. The apps will still occupy space, but it won't be possible to launch them or even find them on the launcher and other lists that show installed apps.

You should be careful when using this option because you might also disable packages that are necessary for proper working of the device or packages that are necessary to make your own kiosk app work properly (you might not even be aware that you require some third-party package, until things in your app break unexpectedly).

To hide an app, you only need to do the following

val cn = ComponentName(this, DevAdminReceiver::class.java)
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager

dpm.setApplicationHidden(cn, packageName, true)

You can see that in the code above I'm using the setApplicationHidden() method of DevicePolicyManager. This method accepts a ComponentName created from our DeviceAdminReceiver that I've implemented in the previous section, and the package name that we want to hide. The last parameter decides if the package should be hidden. That means you can use the same method to unhide a package that was hidden previously.

Note that after calling setApplicationHidden() on a package, you won't be able to list the package with PackageManager's methods, if you only provide default arguments

val packageList = packageManager.getInstalledPackages(0) 

packageList.forEach { packageInfo -> 
    // No hidden packages here
}

But calling these methods with MATCH_UNINSTALLED_PACKAGES should also give you the hidden packages

// Now the list also contains hidden apps
val packageList = packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES or MATCH_DISABLED_COMPONENTS)

You can then check if a package is hidden by calling DevicePolicyManager.isApplicationHidden()

val cn = ComponentName(this, DevAdminReceiver::class.java)
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager

if (dpm.isApplicationHidden(cn, packageName, true)) {
    // This package is hidden
}

Useful Tools

If you're interested in the technical aspects of Android, I recommend to check out my Bugjaeger app.

Bugjaeger allows you to perform various Android maintenance & administration tasks that you normally perform from your laptop computer (including uninstalling apps) directly from your phone. It additionally allows you to execute scripts and get hidden device info that you would normally get only via ADB.

You can even use it to install your kiosk app and activate the device owner mode directly from your phone or table (without having your development machine at hands).

Previous Post

Add a comment