Android安全模式
![[cover-11.png]]
引言
每个Android用户都可能遇到设备行为异常的时刻——应用冻结,系统崩溃,或是更加神秘的软件故障。在这海量复杂的智能手机世界中,有一个隐藏的救世主等待着被召唤:Android的安全模式。就像为您的设备提供一剂急救药一样,安全模式是解决许多常见问题的关键。
在我们的生活中,智能手机早已成为必不可少的伴侣,我们依赖它们来进行通讯、娱乐、工作和探索数字化世界。不过,随着应用数量的增加和设备功能的扩展,系统可能会变得不稳定。这就是安全模式进入游戏的时刻。启用安全模式意味着您的手机将在一个最小化的环境中运行,禁用所有第三方应用,并允许您在不受潜在有害应用或功能影响的情况下,对设备进行故障排除。
在本篇博客中,我们将深入探索Android安全模式的力量和用途。我会指导您如何轻松地进入安全模式,分析安全模式的工作原理,并如何利用它来恢复设备正常运行。无论您是Android初学者还是资深用户,理解如何使用安全模式都是一个宝贵的技能。所以,让我们开始这一旅程,揭开Android安全模式如何成为您紧急救援工具的秘密。
功能入口
安全模式的“入口”,位于SystemServer中,在startOtherServices阶段(前面还有两个大阶段,用于启动核心的Service)进行检测。
final boolean safeMode = wm.detectSafeMode();
检测方法位于WindowManagerService中的detectSafeMode方法,在满足以下任意条件后即可进入Android的安全模式。
进入安全模式的方式
- 检测"MENU"按键是否按下
- 检测"S"按键是否被按下
- 检测"DPAD"按键是否被按下
- 检测鼠标按键是否被按下
- 检测“音量下键”是否被按下(常用)
- 设置了“persist.sys.safemode”属性(持久化属性,进入安全模式后会被自动清除)
- WMS可以以安全模式重启 —— rebootSafeMode
- 设置了“ro.sys.safemode”属性(内存属性,重启后丢失)
⚠️ 除了音量下键,其他的Input事件在手机、平板是不常见的,如果忽视了对Input设备的键码映射,复用了“不存在的设备”,这里可能出现Input异常导致的非正常进入安全模式问题,具体内容我们之后再做分析。
💡通过属性触发,可以帮助厂商更好的定制自己的安全模式。具体内容本篇文章不细谈,未来将单独进行介绍。
DPAD是什么?见下图 ![[Screenshot 2024-03-18 at 13.02.41.png]]
禁用安全模式的方式
- 设置Settings.Global.SAFE_BOOT_DISALLOWED(safe_boot_disallowed)为非0值。
以下是参考代码:
system app或system自身做法:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContentResolver resolver = context.getContentResolver();
Settings.Global.putInt(resolver, "safe_boot_disallowed", 1);
}
DeviceOwner应用的做法:
DevicePolicy添加了DISALLOW_SAFE_BOOT策略后,会自动设置SAFE_BOOT_DISALLOWED属性
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
ComponentName adminName = new ComponentName(context, AdminReceiver.class);
if (dpm.isDeviceOwnerApp(context.getPackageName())) {
dpm.addUserRestriction(adminName, UserManager.DISALLOW_SAFE_BOOT);
}
方法分析
public boolean detectSafeMode() {
// 等待Input设备就绪(超时时间是1000ms)
if (!mInputManagerCallback.waitForInputDevicesReady(
INPUT_DEVICES_READY_FOR_SAFE_MODE_DETECTION_TIMEOUT_MILLIS)) {
...
}
// 判断安全模式是否禁用
if (Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.SAFE_BOOT_DISALLOWED, 0) != 0) {
return false;
}
// 检测MENU按键是否按下(目前大部分机器都没有)
int menuState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_ANY,
KeyEvent.KEYCODE_MENU);
// 检测"S"按键是否被按下
int sState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_ANY, KeyEvent.KEYCODE_S);
// 检测"DPAD"按键是否被按下
int dpadState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_DPAD,
KeyEvent.KEYCODE_DPAD_CENTER);
// 检测鼠标按键是否被按下
int trackballState = mInputManager.getScanCodeState(-1, InputDevice.SOURCE_TRACKBALL,
InputManagerService.BTN_MOUSE);
// 检测“音量下键”是否被按下(常用)
int volumeDownState = mInputManager.getKeyCodeState(-1, InputDevice.SOURCE_ANY,
KeyEvent.KEYCODE_VOLUME_DOWN);
mSafeMode = menuState > 0 || sState > 0 || dpadState > 0 || trackballState > 0
|| volumeDownState > 0;
// 设置了“persist.sys.safemode”或“ro.sys.safemode”属性
try {
if (SystemProperties.getInt(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, 0) != 0
|| SystemProperties.getInt(ShutdownThread.RO_SAFEMODE_PROPERTY, 0) != 0) {
mSafeMode = true;
// 清楚“persist.sys.safemode”属性
SystemProperties.set(ShutdownThread.REBOOT_SAFEMODE_PROPERTY, "");
}
} catch (IllegalArgumentException e) {
}
// 一旦进入安全模式,就会有“ro.sys.safemode”属性标识
if (mSafeMode) {
ProtoLog.i(WM_ERROR, "SAFE MODE ENABLED (menu=%d s=%d dpad=%d"
+ " trackball=%d)", menuState, sState, dpadState, trackballState);
// May already be set if (for instance) this process has crashed
if (SystemProperties.getInt(ShutdownThread.RO_SAFEMODE_PROPERTY, 0) == 0) {
SystemProperties.set(ShutdownThread.RO_SAFEMODE_PROPERTY, "1");
}
} else {
ProtoLog.i(WM_ERROR, "SAFE MODE not enabled");
}
// PhoneWindowManager的setSafeMode,为了通过振动来给用户反馈。
mPolicy.setSafeMode(mSafeMode);
return mSafeMode;
}
// ##### PhoneWindowManager.java
@Override
public void setSafeMode(boolean safeMode) {
mSafeMode = safeMode;
if (safeMode) {
performHapticFeedback(HapticFeedbackConstants.SAFE_MODE_ENABLED, true,
"Safe Mode Enabled");
}
}
安全模式作用
- 自动打开系统的飞行模式
开启飞行模式
// Before things start rolling, be sure we have decided whether
// we are in safe mode.
final boolean safeMode = wm.detectSafeMode();
if (safeMode) {
// If yes, immediately turn on the global setting for airplane mode.
// Note that this does not send broadcasts at this stage because
// subsystems are not yet up. We will send broadcasts later to ensure
// all listeners have the chance to react with special handling.
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 1);
} else if (context.getResources().getBoolean(R.bool.config_autoResetAirplaneMode)) {
Settings.Global.putInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0);
}
// Enable airplane mode in safe mode. setAirplaneMode() cannot be called
// earlier as it sends broadcasts to other services.
// TODO: This may actually be too late if radio firmware already started leaking
// RF before the respective services start. However, fixing this requires changes
// to radio firmware and interfaces.
if (safeMode) {
t.traceBegin("EnableAirplaneModeInSafeMode");
try {
connectivityF.setAirplaneMode(true);
} catch (Throwable e) {
reportWtf("enabling Airplane Mode during Safe Mode bootup", e);
}
t.traceEnd();
}
字体服务
t.traceBegin("StartFontManagerService");
mSystemServiceManager.startService(new FontManagerService.Lifecycle(context, safeMode));
t.traceEnd();
禁用字体更新数据
@Nullable
private UpdatableFontDir createUpdatableFontDir() {
// Never read updatable font files in safe mode.
if (mIsSafeMode) return null;
...
}
AMS
通知AMS进入安全模式,并现实安全模式水印
if (safeMode) {
// 此处会通知PMS进入安全模式
mActivityManagerService.enterSafeMode();
}
...
if (safeMode) {
mActivityManagerService.showSafeModeOverlay();
}
PMS触发安全模式
public final void enterSafeMode() {
synchronized(this) {
// It only makes sense to do this before the system is ready
// and started launching other packages.
if (!mSystemReady) {
try {
AppGlobals.getPackageManager().enterSafeMode();
} catch (RemoteException e) {
}
}
mSafeMode = true;
}
}
在checkPackageStartable方法中,会判断某个包是否可以启动,在进入安全模式的情况下,只有系统App可以正常启动,而非系统App都无法启动。
@PackageManagerService.PackageStartability
@Override
public int getPackageStartability(boolean safeMode, @NonNull String packageName, int callingUid,
@UserIdInt int userId) {
final boolean ceStorageUnlocked = StorageManager.isCeStorageUnlocked(userId);
final PackageStateInternal ps = getPackageStateInternal(packageName);
if (ps == null || shouldFilterApplication(ps, callingUid, userId)
|| !ps.getUserStateOrDefault(userId).isInstalled()) {
return PackageManagerService.PACKAGE_STARTABILITY_NOT_FOUND;
}
if (safeMode && !ps.isSystem()) { // 安全模式下,非System App都不允许启动
return PackageManagerService.PACKAGE_STARTABILITY_NOT_SYSTEM;
}
if (mFrozenPackages.containsKey(packageName)) {
return PackageManagerService.PACKAGE_STARTABILITY_FROZEN;
}
if (!ceStorageUnlocked && !AndroidPackageUtils.isEncryptionAware(ps.getPkg())) {
return PackageManagerService.PACKAGE_STARTABILITY_DIRECT_BOOT_UNSUPPORTED;
}
return PackageManagerService.PACKAGE_STARTABILITY_OK;
}
PMS引起其他服务进入安全模式
- ApplicationPackageManager
- PackageManagerHelper (Launcher3 loadWorkspaceImpl)
- BluetoothManagerService
- ShadowPackageManager
DisplayManager服务
try {
// TODO: use boot phase and communicate this flag some other way
mDisplayManagerService.systemReady(safeMode);
} catch (Throwable e) {
reportWtf("making Display Manager Service ready", e);
}
禁用非必要的显示适配器 shouldRegisterNonEssentialDisplayAdaptersLocked。 进入安全模式后,下面两个方法将不会执行。也就是下面两种显示将会被禁用:
- WiFi Display Adapter(无线网络显示。可以投射屏幕到智能电视、平板等其他屏幕上)
- Overlay Display Adapter(虚拟屏幕。通常用于测试,可以在不使用物理显示设备时模拟屏幕显示。可以用于屏幕录制等)
registerOverlayDisplayAdapterLocked();
registerWifiDisplayAdapterLocked();
ServiceManager
mSystemServiceManager.setSafeMode(safeMode);
VcnGatewayConnectionConfig
VcnGatewayConnection
TrustManagerService
所有代理将不被信任
VoiceInteractionManagerService
禁止第三方语音助理相关功能
ShortcutService
禁止检查Package变化
以下服务将设置安全模式标记
- AppWidgetService
判断进入安全模式的方法
5.1 System Server
public final boolean isSafeMode() {
return getManager().isSafeMode();
}
附录
Zygote运行在安全模式 EnableDebugFeatures
- 禁用JIT