Android 5.1 添加下拉通知栏数据流量开关快捷图标

平台: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 状态栏下拉列表添加自定义item开关

Android5.0 下拉通知栏快捷开关的添加(必看)

Android之TelephonyManager类

Android基础知识(四)-----如何实时监听数据流量开关状态

Android - 判断SIM卡状态

Android SVG的pathData详解

Android vector标签 PathData 画图超详解

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值