Skip to content

需求简介

在学校试点的时候很多学生经常进入浏览器, 浏览一些限制的内容, 希望通过管控的手段, 禁止学生进入浏览器.在系统设置页面可以设置密码或者修改密码、点击浏览器时弹出密码框, 输入正确密码后才可进入浏览器. 若密码错误则提示重新输入或者退出应用.

方案设计

首先我们要找到Launche应用, 然后通过广播接收器拦截启动浏览器的事件, 弹出密码框. 如果密码输入正确, 则允许启动浏览器, 如果密码错误则不允许启动. 同时, 我们还需要提供一个设置页面, 允许用户设置密码或者修改密码.

实现步骤

客制化目錄: FM961L6/CF13/XIAOXING_TABLET_CONTROL

  1. 在Launcher应用中实现浏览器拦截功能 目前定位到该文件packages\apps\Launcher3\src\com\android\launcher3\touch\ItemClickHandler.java是Launcher3中处理点击事件的核心类. 我们可以在这里拦截浏览器启动的事件, 并弹出密码框.
java
public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
    if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
        return;
    }

    // 检查是否是受限应用
    String packageName = shortcut.getIntent().getComponent() != null
            ? shortcut.getIntent().getComponent().getPackageName()
            : shortcut.getIntent().getPackage();
    
    if (isRestrictedApp(packageName)) {
        verifyDeviceControlPassword(v, shortcut, launcher);
        return;
    }

    startAppShortcutOrInfoActivity(v, shortcut, launcher);
}

private static boolean isRestrictedApp(String packageName) {
    // 使用Set提高查找效率
    Set<String> restrictedApps = new HashSet<>(Arrays.asList(
            "com.android.chrome",
            "com.android.browser",
            "org.mozilla.firefox"
            // 可以从配置文件或数据库动态读取
    ));
    return restrictedApps.contains(packageName);
}

private static void verifyDeviceControlPassword(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
    // 从系统设置获取密码
    String correctPassword = Settings.Secure.getString(
            launcher.getContentResolver(), 
            "device_control_password");
    
    if (TextUtils.isEmpty(correctPassword)) {
        // 如果没有设置密码,直接启动应用
        startAppShortcutOrInfoActivity(v, shortcut, launcher);
        return;
    }

    View dialogView = LayoutInflater.from(launcher).inflate(R.layout.password_dialog, null);
    EditText passwordInput = dialogView.findViewById(R.id.password_input);

    AlertDialog dialog = new AlertDialog.Builder(launcher)
            .setTitle(R.string.device_control_title)
            .setView(dialogView)
            .setPositiveButton(R.string.device_control_confirm, null) // 先设为null,避免自动关闭
            .setNegativeButton(R.string.device_control_cancel, null)
            .create();

    dialog.setOnShowListener(dialogInterface -> {
        Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
        button.setOnClickListener(view -> {
            String password = passwordInput.getText().toString();
            if (correctPassword.equals(password)) {
                dialog.dismiss();
                startAppShortcutOrInfoActivity(v, shortcut, launcher);
            } else {
                passwordInput.setText("");
                Toast.makeText(launcher, 
                    R.string.device_control_wrong_password, 
                    Toast.LENGTH_SHORT).show();
            }
        });
    });

    dialog.show();
}

我们还需要创建密码输入框的布局文件-password_dialog.xml

xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/password_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword" />

</LinearLayout>

注意对相关的文字进行国际化,这里仅作为演示 strings.xml 位置: sys\packages\apps\Launcher3\res\values\strings.xml 客制化目录: FM961L6/CF13/XIAOXING_TABLET_CONTROL/sys\packages\apps\Launcher3\res\values\strings.xml

xml
<string name="device_control_password_hint">请输入管控密码</string>
<string name="device_control_title">需要验证</string>
<string name="device_control_confirm">确认</string>
<string name="device_control_cancel">取消</string>
<string name="device_control_wrong_password">密码错误</string>
  1. 在设置页面添加密码设置和修改功能, 可以在系统设置界面添加一个设置项, 允许用户输入密码或者修改密码. 这里我们需要在系统设置界面的1级菜单添加一个设置页面,

packages/apps/Settings/res/xml/top_level_settings.xml 在这个文件中,我们可以新增入口:应用管控, 然后在这个页面中添加密码设置和修改的入口.

xml
<com.android.settings.widget.HomepagePreference
    android:fragment="com.android.settings.devicecontrol.DeviceControlPasswordSettings"
    android:icon="@drawable/ic_lock"
    android:key="device_control_password"
    android:order="-30"
    android:title="@string/device_control_settings_title"
    android:summary="@string/device_control_settings_summary"
    settings:highlightableMenuKey="device_control_password"
    settings:controller="com.android.settings.devicecontrol.DeviceControlSettingsController"/>

这里比较重要的就是fragment页面和controller控制器了

bash
icon: 设置图标
key: 设置项的唯一标识
order: 设置项的排序
title: 设置项的标题
summary: 设置项的摘要
highlightableMenuKey: 高亮显示的菜单项
controller: 控制器类名
fragment: 对应的Fragment类名

然后我们在strings.xml中添加对应的字符串资源

xml
<resources>
    <string name="set_device_control_password">设置管控密码</string>
    <string name="verify_device_control_password">验证管控密码</string>
    <string name="enter_password">输入密码</string>
    <string name="confirm_password">确认密码</string>
    <string name="enter_current_password">输入当前密码</string>
    <string name="wrong_password">密码错误</string>
    <string name="password_empty_error">密码不能为空</string>
    <string name="password_mismatch_error">两次输入的密码不一致</string>
    <string name="device_control_password_not_set">未设置密码</string>
    <string name="device_control_password_set">已设置密码</string>
</resources>

创建 Fragment 类-DeviceControlPasswordSettings.java

java
package com.android.settings.devicecontrol;

import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.widget.EditText;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;

public class DeviceControlPasswordSettings extends DashboardFragment implements
        Preference.OnPreferenceClickListener {
    private static final String TAG = "DeviceControlPasswordSettings";
    private static final String KEY_DEVICE_CONTROL_PASSWORD = "device_control_password";
    private static final String SETTINGS_DEVICE_CONTROL_PASSWORD = "device_control_password";

    private Preference mPasswordPreference;

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        super.onCreatePreferences(savedInstanceState, rootKey);
        mPasswordPreference = findPreference(KEY_DEVICE_CONTROL_PASSWORD);
        if (mPasswordPreference != null) {
            mPasswordPreference.setOnPreferenceClickListener(this);
            updatePasswordSummary();
        }
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.device_control_password_settings;
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.DEVICE_CONTROL_SETTINGS;
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        if (KEY_DEVICE_CONTROL_PASSWORD.equals(preference.getKey())) {
            showPasswordDialog();
            return true;
        }
        return false;
    }

    private void showPasswordDialog() {
        String currentPassword = Settings.Secure.getString(
                getContext().getContentResolver(), SETTINGS_DEVICE_CONTROL_PASSWORD);
        
        if (TextUtils.isEmpty(currentPassword)) {
            showSetPasswordDialog();
        } else {
            showVerifyPasswordDialog();
        }
    }

    private void showSetPasswordDialog() {
        View view = LayoutInflater.from(getContext()).inflate(R.layout.device_control_password_dialog, null);
        EditText passwordEdit = view.findViewById(R.id.password_edit);
        EditText confirmPasswordEdit = view.findViewById(R.id.confirm_password_edit);

        new AlertDialog.Builder(getContext())
                .setTitle(R.string.set_device_control_password)
                .setView(view)
                .setPositiveButton(android.R.string.ok, (dialog, which) -> {
                    String password = passwordEdit.getText().toString();
                    String confirmPassword = confirmPasswordEdit.getText().toString();
                    if (validateNewPassword(password, confirmPassword)) {
                        savePassword(password);
                    }
                })
                .setNegativeButton(android.R.string.cancel, null)
                .show();
    }

    private void showVerifyPasswordDialog() {
        View view = LayoutInflater.from(getContext()).inflate(R.layout.device_control_verify_password_dialog, null);
        EditText passwordEdit = view.findViewById(R.id.password_edit);

        new AlertDialog.Builder(getContext())
                .setTitle(R.string.verify_device_control_password)
                .setView(view)
                .setPositiveButton(android.R.string.ok, (dialog, which) -> {
                    String inputPassword = passwordEdit.getText().toString();
                    if (verifyPassword(inputPassword)) {
                        showSetPasswordDialog();
                    } else {
                        showError(R.string.wrong_password);
                    }
                })
                .setNegativeButton(android.R.string.cancel, null)
                .show();
    }

    private boolean validateNewPassword(String password, String confirmPassword) {
        if (TextUtils.isEmpty(password)) {
            showError(R.string.password_empty_error);
            return false;
        }
        if (!password.equals(confirmPassword)) {
            showError(R.string.password_mismatch_error);
            return false;
        }
        return true;
    }

    private boolean verifyPassword(String password) {
        String savedPassword = Settings.Secure.getString(
                getContext().getContentResolver(), SETTINGS_DEVICE_CONTROL_PASSWORD);
        return !TextUtils.isEmpty(password) && password.equals(savedPassword);
    }

    private void savePassword(String password) {
        Settings.Secure.putString(
                getContext().getContentResolver(), SETTINGS_DEVICE_CONTROL_PASSWORD, password);
        updatePasswordSummary();
    }

    private void updatePasswordSummary() {
        String currentPassword = Settings.Secure.getString(
                getContext().getContentResolver(), SETTINGS_DEVICE_CONTROL_PASSWORD);
        mPasswordPreference.setSummary(TextUtils.isEmpty(currentPassword) 
                ? R.string.device_control_password_not_set
                : R.string.device_control_password_set);
    }

    private void showError(int messageResId) {
        new AlertDialog.Builder(getContext())
                .setMessage(messageResId)
                .setPositiveButton(android.R.string.ok, null)
                .show();
    }
}

创建控制器-DeviceControlSettingsController.java

java
package com.android.settings.devicecontrol;

import android.content.Context;
import com.android.settings.core.BasePreferenceController;

public class DeviceControlSettingsController extends BasePreferenceController {
    
    public DeviceControlSettingsController(Context context, String key) {
        super(context, key);
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }
}

另外我还应该在 sys\packages\apps\Settings\src\com\android\settings\core\gateway\SettingsGateway.java 这个文件中的ENTRY_FRAGMENTS数组中添加我们的fragment, 以便在设置界面显示.(否则系统会报错)

java
public class SettingsGateway {
    // ... existing code ...
    public static final String[] ENTRY_FRAGMENTS = {
        // ... existing fragments ...
        DeviceControlPasswordSettings.class.getName(),
        // ... existing fragments ...
    };
    // ... existing code ...
}

创建设置页面布局:

xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:title="@string/device_control_settings_title">

    <com.android.settings.widget.PasswordEditTextPreference
        android:key="device_control_password"
        android:title="@string/device_control_password_title"
        android:summary="@string/device_control_password_summary"
        settings:controller="com.android.settings.devicecontrol.DeviceControlPasswordController"/>

</PreferenceScreen>

验证

切换到vnd目录,使用source zlunch加载编译环境,使用以下命令编译模块:

bash
#可以看到打开的应用的包名
adb shell "dumpsys window|grep mCurrentFocus"
#根据包名查找模块
adb shell pm path {moduleName}
make XiaoXingLauncher > buildmodule.log
# 查看编译进度并查看是否有编译错误
cat buildmodule.log|grep 100%

或者全局编译

bash
source zlunch
source zmk
./zmkpac.sh
cat build.log|grep 'ninja failed with'
cat build.log|grep 'current path'

Android 14使用模块编译:

bash
# 以前有执行过就不用了
cd zcommon ./create_ln.sh
cd sys/ & make XiaoXingLauncher -j24
# 模块安装

adb root
adb remount

adb reboot

开搞

我拉了个分支XIAOXING_TABLET_CONTROL, 用于测试设备应用管控

  1. 环境编译
source zmake zprj/FM961L6/CF13/XIAOXING_TABLET_CONTROL userdebug 20

结果

后面发现是用户跳转到内置浏览器了,把内置浏览器的网址输入隐藏掉,就满足用户需求了 sys\packages\apps\Browser2\res\layout\activity_webview_browser.xml

xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">
    <!--{@ modify @20250331 for hidden input url entry-->
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone">
    <!--@}-->    
        <EditText
            android:id="@+id/url_field"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1.0"
            android:singleLine="true"
            android:inputType="textUri"
            android:selectAllOnFocus="true"
            android:imeOptions="actionGo"
            android:importantForAutofill="no" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:src="@drawable/breadcrumb_arrow_black"
            android:contentDescription="@string/load_url"
            android:onClick="loadUrlFromUrlBar" />
        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:src="@drawable/item_more_black"
            android:contentDescription="@string/menu_about"
            android:onClick="showPopup" />
    </LinearLayout>
</LinearLayout>

参考链接

  1. Android 系统进入设置项需要输入密码
  2. Settings添加1级菜单并实现跳转