我们平常使用的安卓手机通常都有一个又一个的 app,当你退出某个 app 之后,就会回到一个所谓的”启动器(Launcher)“界面,或者说是home界面,在这个界面中,有其他的好多的app供你启动。

但是在一些场合底下,我们希望自己的安卓设备只启动仅仅一个app,例如火车站的售票程序、ktv的点歌程序、取款机的取款程序。这些程序都是在操作系统之上运行的一个单一程序,对于普通的使用者而言,他们根本就不关心要不要退出该程序,对于开发者而言,我们希望用户没办法退出当前的程序。

这样的程序我们通常称之为 kiosk 程序,当然了,不一定得是安卓系统才能有这样的程序。

那么在安卓系统底下如何开发一个这样的程序呢?

这个程序必须至少满足这样的条件:

  • 全屏运行
  • 用户(普通)无法退出当前程序
  • 在程序运行过程中无法打开安卓系统的通知栏

本文仅考虑 android 5.0 以上的版本实现,也不考虑一些复杂的情况,只完成最简单的实现

# 0. 原理

android 5.0 过后,提供了一种叫做 锁定任务模式(Lock task mode) 的模式,应用程序实现了这种模式之后,可以让您的安卓设备只能运行某几个指定的应用程序。当应用进入了这种模式之后,通常来讲用户就无法访问通知栏、无法访问未经授权的程序、也无法返回home界面(除非home界面是授权的)。

为了进入这种模式,我们需要经过 设备管理者(device policy controller) 的授权。这里的设备管理者不一定是一个人,也可以是一个程序本身,通过调用 DevicePolicyManager.setLockTaskPackages() 的方法来允许某些程序可以进入 lockTaskMode

为了使得一个应用成为设备管理者,我们可以通过调用 adb shelldpm set-device-owner 命令。当然了,只有注册了 DeviceAdminReceiver 的应用程序才可以成为设备管理者

接下来我们就上手操作吧,大致步骤如下:

  1. 注册 DeviceAdminReceiver
  2. 进入全屏模式并启动 LockTaskMode
  3. 通过 adb shell 添加设备管理者
  4. 启动应用

# 1. 注册 DeviceAdminReceiver

可以在您放置 Activity 类 的同目录下创建一个类,例如名为 MyDeviceAdminReceiver,继承 DeviceAdminReceiver 基类。代码如下:

package com.example.tricky;

import android.app.admin.DeviceAdminReceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import androidx.annotation.NonNull;

public class MyDeviceAdminReceiver extends DeviceAdminReceiver {

    @Override
    public void onEnabled(@NonNull Context context, @NonNull Intent intent)     {
        super.onEnabled(context, intent);
    }

}

文件夹结构看起来像这样: 文件夹结构

接着在 AndroidManifest.xml<application> 标签底下添加以下代码

<receiver
    android:name=".MyDeviceAdminReceiver"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_DEVICE_ADMIN">
    <meta-data
        android:name="android.app.device_admin"
        android:resource="@xml/device_admin_receiver" />
    <intent-filter>
        <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
    </intent-filter>
</receiver>

值得注意的是,上面代码片段中的 <meta-data> 标签中的 android:resource 属性,我们需要在 资源文件夹中创建一个 xml 文件夹,并放置一个 device_admin_receiver.xml 文件,该文件内容如下。

<?xml version="1.0" encoding="utf-8"?>
<device-admin>
    <uses-policies>
        <limit-password />
        <watch-login />
        <reset-password />
        <force-lock />
        <wipe-data />
        <expire-password />
        <encrypted-storage />
        <disable-camera />
        <disable-keyguard-features />
    </uses-policies>
</device-admin>

所以资源文件的内容看起来像这样:

资源文件文件夹结构

然后在 AndroidManifest.xml<application> 标签上添加一个 android:testOnly="true" 的标签属性,像这样:

<application android:testOnly="true"></application>

这个属性的作用在第三大点会解释!

# 2. 进入全屏模式并启动 LockTaskMode

假设我们一启动 app 就要进入全屏模式并进入 LockTaskMode,所以我们需要在 入口ActivityonCreate() 添加一些代码:

public class MainActivity extends AppCompatActivity {

    private ComponentName deviceAdminName;
    private DevicePolicyManager dpm;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 进入全屏模式
        Window window = getWindow();
        // 设置系统的ui进入沉浸模式
        window.getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
                View.SYSTEM_UI_FLAG_FULLSCREEN |
                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
        deviceAdminName = new ComponentName(getApplicationContext(),MyDeviceAdminReceiver.class);
        dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);

        // 允许当前程序开启 LockTaskMode,如果当前还没有取得应用管理的权限,这行代码会报错闪退,所以也可以嵌套一个try语句块什么的
        dpm.setLockTaskPackages(deviceAdminName,new String[]{getPackageName()});

        // 启动 LockTaskMode
        this.startLockTask();
    }

    /**
     * 关闭 kiosk,可以给各种按钮调用,用来退出 LockTaskMode
     * @param view
     */
    public void stopKiosk(View view){
        this.stopLockTask();
    }
}

# 3. 通过 adb shell 添加设备管理者

当我们完成了上面代码的书写过后,把程序打包安装进安卓设备,先不要打开应用

打开了也没事,就是会闪退,因为上面的代码片段没有处理这个错误

将手机连接到电脑usb,通过以下 adb shell 命令添加设备管理者:

adb shell dpm set-device-owner com.example.tricky/.MyDeviceAdminReceiver

注意将这行代码中的 com.example.tricky 替换成您自己的程序的包名,还有 .MyDeviceAdminReceiver 替换成第一步时您创建的类名

敲击回车键之后,命令行如果显示了 success 就意味着添加成功了。

不是所有的安卓设备都可以非常方便地添加设备管理者的,因为这样做的风险非常大,这样意味着设备本身的管理可以完全放权给某一个app,所以各个手机厂商的内置系统可能会拦着你。我这里用的是一台root过后的安卓系统测试手机,所以想干嘛都可以~

我们还可以通过以下命令来取消上面设置的设备管理者,前提是我们在 AndroidManifest.xml<application> 标签上添加过 android:testOnly="true" 标签属性。

adb shell dpm remove-active-admin com.example.tricky/.MyDeviceAdminReceiver

# 4. 启动应用

完成!接下来的开发就看你了。一旦应用进入了 LockTaskMode ,可以通过执行 stopLockTask() 方法来退出这个模式。您可以设置一个界面密码来允许开发者或维护人员暂时性地退出该模式。

# 参考文献

  1. 安卓开发者文档 - 锁定任务模式
  2. How to turn your Android application into a kiosk
  3. 安卓开发者文档 - 启用全屏模式