在很多app里会存在这种需求, 就是引导用户跳转到系统设置里的某一页. 对于大多数要求不严格的需求, 直接根据Action就可以跳转到指定页的最外层, 比如WIFI设置, 辅助权限开启, 等等, 都可以跳转到对应模块的最外层页面. 但为了提高用户体验, 直接跳转到最具体的页面是最好的实现. 举例, 在我最近做的一个项目中, 需要引导用户跳转开启辅助权限, 类似豌豆荚的一键安装需求一样, 都要用户开启辅助服务的权限, 然后通过模拟点击实现一些功能. 对比此类需求的一些app, 发现它们在引导用户去开启此权限的时候都不能跳转到最具体的一页, 而是其上一级页面. 这个时候通常会出现一个浮层或者弹窗, 来提示用户, XXX向下滑, 找到XXX, 点击开启. 这种体验个人感觉特别差, 对于哪些对辅助权限依赖高的app, 这种体验会直接影响其可用性. 所以为了解决这个问题, 为了能直接跳转到最具体的页面, 就只好反编译系统Setting, 并从中找到答案.
- odex 反编译
- 从反编译后的Setting源码中找答案
odex反编译
odex是为了加快系统启动速度而对dex优化后的格式.
在早期android系统版本里, 系统应用只有apk格式. 后续, 为了优化系统启动速度, apk被分割成apk和odex两个文件, 其中运行代码在odex中. 所以反编译Setting代码只需反编译其中的Settings.odex即可.下面以Samsung 4.4的系统为例子, 来讲解具体的步骤.
工具
- adb
- baksmali2.1.0.jar (下载地址:https://bitbucket.org/JesusFreke/smali/downloads)
- smali2.1.0.jar (下载地址:https://bitbucket.org/JesusFreke/smali/downloads)
- dex2jar (下载地址:https://github.com/pxb1988/dex2jar)
- jd-gui (下载地址:https://github.com/java-decompiler/jd-gui/releases/download/v1.4.0/jd-gui-1.4.0.jar)
反编译步骤
- 用adb pull /system/priv-app/SecSettings.odex (原生叫Settings) 从手机中拉出待反编译的odex文件.
执行java -jar baksmali-2.1.0.jar -x SecSettings.odex -d .
此时, 会抛出异常
Error occured while loading boot class path files. Aborting.
org.jf.util.ExceptionWithContext: Cannot locate boot class path file /system/framework/core.odex因为, SecSettings.odex会依赖一些framework里相关的odex. 所以需要一一使用adb pull /system/framework/xxx.odex 将它们拷贝到当前目录. 并重新执行步骤2, 依次循环, 直至反编译成功.
- 步骤2成功后, 会在当前目录生成一个out目录. 此时, 执行
java -jar smali-2.1.0.jar -o classes.dex out - 步骤3成功后, 会生成classes.dex 文件. 使用dex2jar工具来将dex转成jar文件.
- 使用jd-gui工具即可查看jar中classes对应的java类文件.
- 步骤2成功后, 会在当前目录生成一个out目录. 此时, 执行
至此, 反编译完成!
从反编译后的Setting源码中找答案
使用adb shell dumpsys activity 命令, 可以得知Samsung 4.4 中的设置应用的主界面对应的activity为GridSettings(原生的是Settings). 找到该类, 并打开分析其启动代码.
由于GridSettings继承PreferenceActivity, 在PreferenceActivity的oncreate里, 找到如下代码:
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
int initialTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, 0);
int initialShortTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, 0);
if (savedInstanceState != null) {
// We are restarting from a previous saved state; used that to
// initialize, instead of starting fresh.
ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG);
if (headers != null) {
mHeaders.addAll(headers);
int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG,
(int) HEADER_ID_UNDEFINED);
if (curHeader >= 0 && curHeader < mHeaders.size()) {
setSelectedHeader(mHeaders.get(curHeader));
}
}
} else {
if (initialFragment != null && mSinglePane) {
// If we are just showing a fragment, we want to run in
// new fragment mode, but don't need to compute and show
// the headers.
switchToHeader(initialFragment, initialArguments);
即, 我们想通过, 启动Setting的activity, 并设置intent的方式来启动具体的页面, 那么必需要经过该流程, 即, 从getIntent里拿到要show的fragment, 然后调用, switchToHeader.
public void switchToHeader(String fragmentName, Bundle args) {
Header selectedHeader = null;
for (int i = 0; i < mHeaders.size(); i++) {
if (fragmentName.equals(mHeaders.get(i).fragment)) {
selectedHeader = mHeaders.get(i);
break;
}
}
setSelectedHeader(selectedHeader);
switchToHeaderInner(fragmentName, args);
}
其中, 最后会调用switchToHeaderInner方法.
private void switchToHeaderInner(String fragmentName, Bundle args) {
getFragmentManager().popBackStack(BACK_STACK_PREFS,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
if (!isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Fragment.instantiate(this, fragmentName, args);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
transaction.replace(com.android.internal.R.id.prefs, f);
transaction.commitAllowingStateLoss();
}
在show出我们想启动的具体页面的时候, 首先会判断该Fragment是否有效. 这里调用的是GridSettings重写的isValidFragment方法来判断.其实现如下:
protected boolean isValidFragment(String paramString)
{
int i = 0;
while (i < ENTRY_FRAGMENTS.length)
{
if (ENTRY_FRAGMENTS[i].equals(paramString)) {
return true;
}
i += 1;
}
return false;
}
代码很简单, 即查找ENTRY_FRAGMENTS列表里是否存在我们要显示的Fragment名. ENTRY_FRAGMENT定义在该类里:
ENTRY_FRAGMENTS = { WirelessSettings.class.getName(), WifiSettings.class.getName(), AdvancedWifiSettings.class.getName(), BluetoothSettings.class.getName(), TetherSettings.class.getName(), WifiP2pSettings.class.getName(), VpnSettings.class.getName(), DateTimeSettings.class.getName(), LocalePicker.class.getName(), InputMethodAndLanguageSettings.class.getName(), SpellCheckersSettings.class.getName(), UserDictionaryList.class.getName(), UserDictionarySettings.class.getName(), OneHandEditMenu.class.getName(), OneHandSideSoftKeyFragment.class.getName(), SoundSettings.class.getName(), DisplaySettings.class.getName(), DeviceInfoSettings.class.getName(), ManageApplications.class.getName(), ProcessStatsUi.class.getName(), NotificationStation.class.getName(), AppOpsSummary.class.getName(), LocationSettings.class.getName(), SecuritySettings.class.getName(), PrivacySettings.class.getName(), DeviceAdminSettings.class.getName(), AccessibilitySettings.class.getName(), CaptionPropertiesFragment.class.getName(), TextToSpeechSettings.class.getName(), Memory.class.getName(), DevelopmentSettings.class.getName(), UsbSettings.class.getName(), AndroidBeam.class.getName(), WifiDisplaySettings.class.getName(), PowerUsageSummary.class.getName(), AccountSyncSettings.class.getName(), CryptKeeperSettings.class.getName(), DataUsageSummary.class.getName(), DreamSettings.class.getName(), UserSettings.class.getName(), NotificationAccessSettings.class.getName(), ManageAccountsSettings.class.getName(), PrintSettingsFragment.class.getName(), PrintJobSettingsFragment.class.getName(), TrustedCredentialsSettings.class.getName(), PaymentSettings.class.getName(), "com.android.settings.safetycare.SafetyCareSettings", KeyboardLayoutPickerFragment.class.getName(), SmartBondingSettings.class.getName(), ToolboxMenu.class.getName(), ToolboxList.class.getName(), SMotionGuideHub2014.class.getName(), AirplaneModeSettings.class.getName(), NfcSettings.class.getName(), NfcOsaifukeitaiSettings.class.getName(), NearbySettings.class.getName(), WallpaperSettings.class.getName(), LockscreenMenuSettings.class.getName(), MultiWindowSettings.class.getName(), NotificationPanelMenu.class.getName(), OneHandSettings.class.getName(), EasyMode.class.getName(), DormantmodeSettings.class.getName(), MotionSettings2014.class.getName(), FingerAirViewSettings.class.getName(), FingerAirViewHelp.class.getName(), PenAirViewHelp.class.getName(), AccountMenu.class.getName(), DockSettings.class.getName(), LaunchApplication.class.getName(), PersonalPageSettings.class.getName(), "com.nttdocomo.android.docomoset.DocomoServiceSetting", "com.android.settings.DCMHomeSettings", ToddlerModeSettings.class.getName(), ApplicationsSettingsVZW.class.getName(), "com.android.settings.festivaleffect.FestivalEffectSettings" };
以上为所有的有效的Fragment类名. 与原生的代码对比, 其中过滤了一些Fragment, 如, 我们需要跳转到辅助服务权限开启最里层的页面ToggleAccessibilityServicePreferenceFragment. 所以, 答案变揭晓了, 在samsung4.4上, 并不能实现直接跳转到此页面!!!
至此, 该文开头提出的问题便有了结论, 根据我们想要跳转的具体页面的Fragment的类名, 在Setting主页面Activity中定义的ENTRY_FRAGMENTS看是否存在, 如果存在, 即可以直接跳转进入, 反之则不能!