平台:RK3288 Android5.1
需求:Android原生的系统下拉通知栏的快捷方式中有一个sim卡的图标,点击会进入流量使用详情界面,客户想将这个图标换成手机那样直接开关数据流量的按钮。
思路:下拉通知栏属于systemUI,所以要修改需要去到SystemUI的源码位置(frameworks/base/packages/SystemUI/)去修改,因为实现的是开关的功能,所以可以参考gps开关的方式,点击响应事件部分和显示部分做对应的修改就行了。
步骤:
(1)
先来看看下拉状态栏快捷方式的布局,查资料找到是在frameworks/base/packages/SystemUI/res/values/config.xml
中:
<string name="quick_settings_tiles_default_bt" translatable="false">
wifi,bt,inversion,cell,airplane,rotation,flashlight,location,cast,hotspot
</string>
可见有好几个选项,我们要改的是cell对应的选项,我们可以选择在这里替换掉它,改成networkdata
(2)
然后再看是哪里会读取这个xml文件,找到是在frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
的loadTileSpecs方法中调用了,在createTile方法中会对这个属性中的值做匹配,找到:
else if (tileSpec.equals("cell")) return new CellularTileForSlot(this, PhoneConstants.SIM_ID_1);
可见当解析到cell这个项的时候,会构造一个CellularTileForSlot类,因为上一步中把cell值替换为了networkdata,所以在这里也要添加一个case:
else if (tileSpec.equals("networkdata")) return new xxx;
当然从这一步看,我们也可以选择在上一步中选择不替换cell,而是在这里这个case中选择替换掉return后面的类。因为代码需要兼容不同的项目,在xml文件中兼容比在java文件中兼容难度大的做,所以笔者选择不做(1)中的替换,而是在
else if (tileSpec.equals("cell")) return new CellularTileForSlot(this, PhoneConstants.SIM_ID_1);
中选择替换了return后面的类。
(3)
因为我们要做的功能与gps开关类似,作为参考,找到这个开关的实现:
else if (tileSpec.equals("location")) return new LocationTile(this);
搜索这个类,位置为:
frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
所以在同一个目录下,复制此文件,并且更名为NetworkDataTile.java,与类名相关的比如构造函数等都做修改,其他先不变,(2)中的
else if (tileSpec.equals("cell")) return new CellularTileForSlot(this, PhoneConstants.SIM_ID_1);
修改为:
else if (tileSpec.equals("cell")) return new NetworkDataTile(this);
并且import对应的类。
编译,没有出现问题,将生成的apk导入系统对应位置,重启,下拉通知栏发现果然原来sim卡的标志已经变成了location的标志。
这样就成功迈出第一步。
(4)
接下来先不管UI,先搞定功能,这是一个类似按钮的控件,所以需要找到点击事件,通过对比LocationTile.java,发现了其中点击事件的逻辑都是在handleClick这个函数之中,所以只要修改这里就可以修改点击功能。先看LocationTile里面的逻辑:
@Override
protected void handleClick() {
final boolean wasEnabled = (Boolean) mState.value;
mController.setLocationEnabled(!wasEnabled);
mEnable.setAllowAnimation(true);
mDisable.setAllowAnimation(true);
refreshState();
}
整段最核心的地方就两句,一是:
mController.setLocationEnabled(!wasEnabled);
从名称来看就是这句用来设置GPS的开关,具体怎么实现,因为不需要修改,暂时就不深入研究了。这句话的意思就是设置与当前状态相反的状态。
还有一句是
refreshState();
这句应该是更新UI,在LocationTile中并没有找到对应的函数实现,那就只能往上找父类了,LocationTile继承的是QSTile,QSTile文件位置为:
frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
在这个类中找到了具体实现为:
protected final void refreshState() {
refreshState(null);
}
protected final void refreshState(Object arg) {
mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
}
这个实现使用了Handler,mHandler定义为:
protected final H mHandler;
那再找类H:
protected final class H extends Handler {
......
@Override
public void handleMessage(Message msg) {
......
} else if (msg.what == REFRESH_STATE) {
name = "handleRefreshState";
handleRefreshState(msg.obj);
}
......
}
......
}
在handleRefreshState中实现:
protected void handleRefreshState(Object arg) {
handleUpdateState(mTmpState, arg);
final boolean changed = mTmpState.copyTo(mState);
if (changed) {
handleStateChanged();
}
}
所以具体的可以在
abstract protected void handleUpdateState(TState state, Object arg);
接口实现,在LocationTile中就实现了这个接口:
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
final boolean locationEnabled = mController.isLocationEnabled();
state.visible = !mKeyguard.isShowing();
state.value = locationEnabled;
if (locationEnabled) {
state.icon = mEnable;
state.label = mContext.getString(R.string.quick_settings_location_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_location_on);
} else {
state.icon = mDisable;
state.label = mContext.getString(R.string.quick_settings_location_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_location_off);
}
}
这个函数是对UI做处理,是根据当前GPS状态显示不同的UI,值得注意的几个是:
state.visible :用来判断UI是否显示
state.value :用来储存状态值
state.icon :UI的图标
state.label :UI的图标下面的label
(5)
现在了解了大体显示和点击流程了,那大概只要修改handleUpdateState和handleClick这两个函数就可以了。
还是先改功能不管UI,但是UI的判断条件需要改为对应的当前数据流量开关的状态。判断当前的数据流量状态可以用TelephonyManager的getDataEnabled()函数判断,进入
frameworks/base/telephony/java/android/telephony/TelephonyManager.java
发现这个函数是 @SystemApi,SystemUI调用没有问题。
添加一个类成员变量:
TelephonyManager mTelephonyManager;
TelephonyManager的初始化可以放在构造函数中进行:
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
在handleUpdateState中获取当前状态并且将状态存储起来:
final boolean dataEnabled = mTelephonyManager.getDataEnabled();
state.value = dataEnabled;
然后将(4)中判断的变量从locationEnabled改为dataEnabled,UI部分不做改动。然后修改点击事件的逻辑为:
final boolean wasEnabled = (Boolean) mState.value;
mTelephonyManager.setDataEnabled(!wasEnabled);
refreshState();
编译,没有出现问题,导入机器重启,点击按钮,果然实现了开关数据网络的功能。
(6)
开关的功能实现了,但是因为点击的时候只刷新一次UI,所以对于开启数据网络这样的耗时操作,调用refreshState()的时候,getDataEnabled()返回的值还来不及改变,所以UI不刷新,实际上值已经改变了,这样的交互很不友好,所以需要对网络状态做实时监听,当完全开启了数据网络之后,重新刷新一遍。于是添加:
@Override
public void setListening(boolean listening) {
mTelephonyManager.listen(phoneStateListener,
PhoneStateListener.LISTEN_DATA_CONNECTION_STATE);
}
private PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onDataConnectionStateChanged(int state) {
super.onDataConnectionStateChanged(state);
refreshState();
}
};
setListening是QSTile实现的一个接口,一般监听的初始化都在这个函数实现。
编译后将生成文件导入机器重启,点击开启之后,一段时间数据网络开启完毕,图标果然有再次刷新,功能实现。
(7)
已实现的功能还有一些小问题,比如没有插卡或者刚刚开机卡还没准备好的时候仍然能够点击,在开启的过程中能重复点击。所以还需要通过TelephonyManager的getSimState()获取sim卡状态,返回的值有以下几个:
SIM_STATE_UNKNOWN
SIM_STATE_ABSENT
SIM_STATE_PIN_REQUIRED
SIM_STATE_PUK_REQUIRED
SIM_STATE_NETWORK_LOCKED
SIM_STATE_READY
SIM_STATE_NOT_READY
SIM_STATE_PERM_DISABLED
SIM_STATE_CARD_IO_ERROR
在TelephonyManager中的定义为:
public static final int SIM_STATE_UNKNOWN = 0;
/** SIM card state: no SIM card is available in the device */
public static final int SIM_STATE_ABSENT = 1;
/** SIM card state: Locked: requires the user's SIM PIN to unlock */
public static final int SIM_STATE_PIN_REQUIRED = 2;
/** SIM card state: Locked: requires the user's SIM PUK to unlock */
public static final int SIM_STATE_PUK_REQUIRED = 3;
/** SIM card state: Locked: requires a network PIN to unlock */
public static final int SIM_STATE_NETWORK_LOCKED = 4;
/** SIM card state: Ready */
public static final int SIM_STATE_READY = 5;
/** SIM card state: SIM Card is NOT READY
*@hide
*/
public static final int SIM_STATE_NOT_READY = 6;
/** SIM card state: SIM Card Error, permanently disabled
*@hide
*/
public static final int SIM_STATE_PERM_DISABLED = 7;
/** SIM card state: SIM Card Error, present but faulty
*@hide
*/
public static final int SIM_STATE_CARD_IO_ERROR = 8;
当sim卡未准备好时,不允许点击,所以handleClick中点击事件修改为:
if(mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY)
{
final boolean wasEnabled = (Boolean) mState.value;
mTelephonyManager.setDataEnabled(!wasEnabled);
refreshState();
}
当sim卡未准备好时,默认显示的是关闭的状态,则handleUpdateState中将
if (dataEnabled)改为if (dataEnabled && mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY)
为了防止修改过程中重复点击,则设置一个boolean值isDone作为flag,默认为true,点击时设置为false,在检测到状态改变即操作完成时再置为true。故handleClick中修改为:
if(mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY && isDone)
{
final boolean wasEnabled = (Boolean) mState.value;
mTelephonyManager.setDataEnabled(!wasEnabled);
isDone = false;
refreshState();
}
phoneStateListener的onDataConnectionStateChanged改为:
private PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onDataConnectionStateChanged(int state) {
super.onDataConnectionStateChanged(state);
isDone = true;
refreshState();
}
};
至此,逻辑就完整了。
(8)
功能实现了,现在要修改UI了,之前是用LocationTile的UI:
private final AnimationIcon mEnable =
new AnimationIcon(R.drawable.ic_signal_location_enable_animation);
private final AnimationIcon mDisable =
new AnimationIcon(R.drawable.ic_signal_location_disable_animation);
if (locationEnabled) {
state.icon = mEnable;
state.label = mContext.getString(R.string.quick_settings_location_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_location_on);
} else {
state.icon = mDisable;
state.label = mContext.getString(R.string.quick_settings_location_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_location_off);
}
在values/strings.xml添加所需的字符串,然后将这些字符串替换之前LocationTile用的字符串,主要替换掉state.label和state.contentDescription的即可,在这里不再赘述。
而图标,LocationTile用的是SystemUI封装的一个类AnimationIcon,是动画,去drawble路径下查看ic_signal_location_enable_animation.xml的资源:
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_signal_location_enable" >
发现是调用了ic_signal_location_enable.xml的资源,这个资源下图片是通过SVG绘制的:
//截取一段示例
......
<group
android:name="cross" >
<path
android:name="cross_1"
android:pathData="M 6.44050598145,1.9430847168 c 0.0,0.0 33.5749816895,33.4499664307 33.5749816895,33.4499664307 "
android:strokeColor="#FFFFFFFF"
android:strokeAlpha="1"
android:strokeWidth="3.5"
android:fillColor="#00000000" />
</group>
......
因为对SVG不太熟悉,所以干脆用ps弄了两张图片:
data_enable.png
data_disable.png
放在了drawable下,使用代码直接获取,在handleUpdateState的实现为:
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
final boolean dataEnabled = mTelephonyManager.getDataEnabled();
state.visible = true;
state.value = dataEnabled;
if (dataEnabled && mTelephonyManager.getSimState() == TelephonyManager.SIM_STATE_READY) {
state.icon = ResourceIcon.get(R.drawable.data_enable);
state.label = mContext.getString(R.string.quick_settings_network_data_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_network_data_on);
} else {
state.icon = ResourceIcon.get(R.drawable.data_disable);
state.label = mContext.getString(R.string.quick_settings_network_data_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_network_data_off);
}
}
至此,所需功能完全实现。
注意:
设置数据流量状态以及获取sim卡状态可以参考应用的做法。这些方法都是系统应用才能调用,需要添加权限:
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
并且在AndroidManifest中添加
android:sharedUserId=”android.uid.system”
参考文章:
Android 通过代码实现控制数据网络的开关(仅适用于5.0以上)
Android基础知识(四)-----如何实时监听数据流量开关状态
Android vector标签 PathData 画图超详解