系统设置是Android系统中非常重要的系统应用,也是整个Android系统的控制中枢,关于设备的硬件,状态,软件,安全等都需要通过系统设置进行控制和查看。比如wifi状态,网络连接状。特别的,系统设置并不同于大多数的其他系统应用,系统设置不仅拥有platform签名(即系统签名),而且属于内核应用,所以系统设置要比非内核应用的系统应用有更大的权限。
一、系统设置的编译与权限
任何一个系统应用都会有一个Adroid.mk文件,该文件用于定义如何编译当前程序,所以在分析系统设置的源代码之前,我们首先应该看一下Android.mk文件的内容。系统设置的Android.mk文件内容如下:
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_JAVA_LIBRARIES := bouncycastle conscrypt telephony-common ims-common
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 android-support-v13 jsr305
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := \
$(call all-java-files-under, src) \
src/com/android/settings/EventLogTags.logtags
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
//编译后将会声称的apk文件的名字 此处即:Settings.apk文件
LOCAL_PACKAGE_NAME := Settings
//指定平台签名
LOCAL_CERTIFICATE := platform
LOCAL_PRIVILEGED_MODULE := true
//指定混淆标志文件 proguard.flags文件和Android.mk处于同一目录下,可以在源码中打开查看。就是一些混淆规则的配置
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
include frameworks/opt/setupwizard/navigationbar/common.mk
include $(BUILD_PACKAGE)
# Use the following include to make our test apk.
ifeq (,$(ONE_SHOT_MAKEFILE))
include $(call all-makefiles-under,$(LOCAL_PATH))
endif
上一篇分析PackageInstaller的时候说过静默安装和静默卸载需要保证应用是系统应用。这里就讲一下如何将某个应用变成系统应用:
1. 应该是使用平台签名,例如此处的Settings.apk的Android.mk文件中指定的签名平台是platform即系统平台签名,所以在签名的时候会使用系统的签名文件进行签名。platform对应的系统签名文件的位置为:
android源码根目录/build/target/product/security/platform.pk8和android源码根目录/build/target/product/security/platform.x509.pem 两个文件
2. 在应用工程的清单配置文件AndroidManifest.xml文件中指定共享用户ID,并将coreApp属性设置为true
3. ndroid源码中使用mm/mmm命令进行编译,其权限就会与系统设置一致;
4. 将apk文件复制到Android设备的/system/app目录中
二、实现修改Android系统的开机动画
Android系统中,与开机动画相关的文件都放在一个叫bootanimation.zip的压缩包文件内,bootanimation.zip随system.img一起发布。需要放到system/media目录中.如果我们要查找系统自带的bootanimation.zip文件,可以将system目录还原,在其下的media目录下存在一个bootanimation.zip文件,替换该文件,我们就能完成开机动画的修改。bootanimation.zip文件解压看看里面的具体包含的内容:首先包含一个desc.txt文件,还会包含若干类似part0,part1的目录,其中desc.txt文件是必须的,part0、part1等目录至少要存在一个。part0、part1目录中存放的是图像文件,诸如001.png、002.png类似的命名有规律的图像文件。Android系统中读取这些静态图像,并按一定的显示规律,和频率产生动画效果,desc.txt文件就是用来描述加载规律和频率信息的文件。
为了使Android拥有Root权限,需要在代码中执行su命令。在执行su命令的过程中会创建一个新的拥有root权限的进程,通过该进程进行的任何操作都是在root权限下进行的
//执行su命令,并创建一个新进程(Process对象)
Process process = Runtime.getRuntime().exec("su");
//获取新进程的OutputStream对象,可以通过该对象发出要执行的命令
OutputStream os = process.getOutputStream();
//获取新进程的InputStream对象,可以通过该对象获取命令执行后返回的数据
InputStream is = process.getInputStream();
......
最后还要实现设备的重启,在Android系统中,要实现重启有两种方式:
1. 执行reboot命令.执行reboot命令需要root权限。也就是说,只要拥有了root权限,任何应用程序都可以重启Android设备
2. 调用PowerManager.reboot命令。该种重启方式只有System用户能使用,自由system用户才允许设置android.permission.REBOOT权限
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
pm.reboot("change boot animation");
最后执行重启设备命令需要在清单配置文件中配置android.permission.REBOOT权限
三、寻找Settings的入口
和安装和卸载应用程序不同的,系统设置Setting在系统桌面中有应用图标。我们寻找程序入口,第一步查看程序配置清单,我们可以根据Activity的intent-filter进行过滤。找到包含如下Action和Category的Intent-Filter,该Intent-Filter所在的窗口就是主窗口。
<activity android:name="../../Settings">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
在5.1.1的Settings源码中,Setting extends SettingActivity.查看SettingActivity发现,在SettingActivity中加载了设置的主界面布局,名为dashboard_categories,文件目录为项目目录/res/xml/dashboard_categories文件,布局文件中使用的布局标签为以及列表项,SettingsActivity中加载该布局文件的方法为loadCategoriesFromResource(),该方法存在于buildDashboardCategories(List categories)方法中,其中的行参就是所包含的设置列表项,加载该xml布局的方法就是解析xml解析器,对布局文件进行解析,现将该方法贴在此处,顺便回顾一下解析xml文件的方法:
//resid 为布局文件的id, List集合为数据源
private void loadCategoriesFromResource(int resid, List<DashboardCategory> target) {
//创建XML文件解析器对象
XmlResourceParser parser = null;
try {
parser = getResources().getXml(resid);
//获取xml文件的所有节点
AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.START_TAG) {
// Parse next until start tag is found
}
String nodeName = parser.getName();
if (!"dashboard-categories".equals(nodeName)) {
throw new RuntimeException(
"XML document must start with <preference-categories> tag; found"
+ nodeName + " at " + parser.getPositionDescription());
}
Bundle curBundle = null;
final int outerDepth = parser.getDepth();
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
//获取节点的名称
nodeName = parser.getName();
//dashboard-category节点 每一项的开始节点 5.1.1中共有4个 分别是WIRELESS and NETWORKS、DEVICE、PERSONAL、SYSTEM
if ("dashboard-category".equals(nodeName)) {
DashboardCategory category = new DashboardCategory();
TypedArray sa = obtainStyledAttributes(
attrs, com.android.internal.R.styleable.PreferenceHeader);
category.id = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_id,
(int)DashboardCategory.CAT_ID_UNDEFINED);
TypedValue tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
category.titleRes = tv.resourceId;
} else {
category.title = tv.string;
}
}
sa.recycle();
final int innerDepth = parser.getDepth();
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
//每一项中的每一小项的节点
String innerNodeName = parser.getName();
//每一小项的节点dashboard-tile 如:wifi,apps,battery等具体的小项
if (innerNodeName.equals("dashboard-tile")) {
DashboardTile tile = new DashboardTile();
sa = obtainStyledAttributes(
attrs, com.android.internal.R.styleable.PreferenceHeader);
tile.id = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_id,
(int)TILE_ID_UNDEFINED);
tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_title);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
tile.titleRes = tv.resourceId;
} else {
tile.title = tv.string;
}
}
tv = sa.peekValue(
com.android.internal.R.styleable.PreferenceHeader_summary);
if (tv != null && tv.type == TypedValue.TYPE_STRING) {
if (tv.resourceId != 0) {
tile.summaryRes = tv.resourceId;
} else {
tile.summary = tv.string;
}
}
tile.iconRes = sa.getResourceId(
com.android.internal.R.styleable.PreferenceHeader_icon, 0);
tile.fragment = sa.getString(
com.android.internal.R.styleable.PreferenceHeader_fragment);
sa.recycle();
if (curBundle == null) {
curBundle = new Bundle();
}
final int innerDepth2 = parser.getDepth();
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth2)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String innerNodeName2 = parser.getName();
if (innerNodeName2.equals("extra")) {
getResources().parseBundleExtra("extra", attrs, curBundle);
XmlUtils.skipCurrentTag(parser);
} else if (innerNodeName2.equals("intent")) {
tile.intent = Intent.parseIntent(getResources(), parser, attrs);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
if (curBundle.size() > 0) {
tile.fragmentArguments = curBundle;
curBundle = null;
}
// Show the SIM Cards setting if there are more than 2 SIMs installed.
if(tile.id != R.id.sim_settings || Utils.showSimCardTile(this)){
category.addTile(tile);
}
} else {
XmlUtils.skipCurrentTag(parser);
}
}
//将category 添加至 数据源集合
target.add(category);
} else {//否则跳过当前的标签
XmlUtils.skipCurrentTag(parser);
}
}
} catch (XmlPullParserException e) {
throw new RuntimeException("Error parsing categories", e);
} catch (IOException e) {
throw new RuntimeException("Error parsing categories", e);
} finally {
if (parser != null) parser.close();
}
}
如上的加载和解析的方法其实是在onResume方法中调用的,在onResume方法中调用了invalidateCategories方法,在invalidateCategories方法中我们会发现其实是采用了回发消息然后处理的消息处理机制,在此方法中发送了MSG_BUILD_CATEGORIES消息,在类声明处定义的名为mHandler的Handler中进行处理,随即调用buildDashboardCategories方法.
下面我们重新说回设置界面,上面说到解析和加载xml文件及相关数据展示,下面设计到的就是我们点击每一项时所出发的跳转事件了。因为加载和解析是在上面所贴代码方法中实现的,所以我们到该方法中查找事件监听方法,但是却未找到;我们转而去xml文件中进行查看,可以发现具体的每一项的配置大致如下,如WIFI:
<!-- Wifi -->
<dashboard-tile
android:id="@+id/wifi_settings"
android:title="@string/wifi_settings_title"
android:fragment="com.android.settings.wifi.WifiSettings"
android:icon="@drawable/ic_settings_wireless"
/>
id,title自然不别说,分别是特定的id,以及所对应的title,另外我们还可以猜到icon就是该项所对应的显示图标。如上代码,还剩下一个标签fragment,其配置的值是一个java文件,到此我们就明白了,当有点击事件发生时,会跳转到fragment中配置的对应的文件中。另外,像我们之前想要修改开机动画一样,如果我们愿意,我们可以自由的添加属于自己的系统属性。我们只需要将想要配置的系统选项按如上格式配置到名为dashboard_categories的xml文件中即可。
系统设置应用是Android系统中非常重要的一个系统应用,接下来在下一篇中会分析一些具体的选项的配置,如何实现具体的属性设置,系统的实现原理等内容。