Android4.42-Settings源码分析之蓝牙模块Bluetooth(上)

继上一篇  Android系统源码剖析(一)---Settings


接着来介绍一下设置中某个模块的源码,本文依旧是基于Android4.42源码进行分析,分析一下蓝牙模块的实现。建议大致看一下关于Settings的剖析。


ZERO,蓝牙模块的fragment及其配置

     1>,首先由Settings_headers.xml文件可以知道,蓝牙对应的fragment为BluetoothSettings.java,对应的id,icon,title,不再赘述,可自行查看xml文件即可



    
       2>,所涉及到的清单配置文件中的属性详解,清单文件中介绍了蓝牙界面启动相关的一些设置,诸如有快捷方式入口,以及是否隐藏进程等等,在这里大致对一些不常见的属性进行说明,方便查阅


            
               ......
            
            
            
        

        
        
            
            
        

可以看到Bluetooth涉及到两个activity节点,一个是activity,还有一个是activity-alias(activity的别名,用于兼容旧版的快捷方式)

  • android:uiOptions="splitActionBarWhenNarrow"     // 关于导航栏actionbar的配置,在此配置为当屏幕宽度不够时控件自动显示在屏幕底部
  • android:configChanges="orientation|keyboardHidden|screenSize"  //用于禁止横竖屏切换,这个属性有几个问题需要好好说一下:第一,若不设置该属性,则切屏时会重新调用各个生命周期,切横屏调用一次,切竖屏则需要调用两次。第二,如果设置了该属性android:configChanges="orientation|keyboardHidden,则不会重新调用生命周期只会执行onConfigurationChanged方法。第三,第二条说法成立的条件是必须是Android3.2以下的版本,如果高于该版本,则必须在该属性后加上screensize(屏幕的size),才会起作用。
  • android:taskAffinity=""   //用于指定创建该activity后用于进入的栈,如果未指定该属性,则就照application节点下指定的栈,如果application也未显示的指定,则为默认的包下。
  • android:excludeFromRecents="true"   //是否显示在最近启动的程序列表中,设为true表示不显示。手机长按home键可以看到最近的程序列表,用此属性可以隐藏进程
  • 可以看到有一个与activity并列的节点。该节点属于activity的别名,目标activity不会覆盖该节点下的属性,而且,针对目标activity设置的属性会自动添加到activity-alias节点下,也就是说蓝牙模块满足两个节点下的属性,之所以有别名进行属性设置,主要是为了兼容旧的快捷方式
  • android:targetActivity="Settings$BluetoothSettingsActivity"   //由快捷方式进入所启动的activity
  • android:exported="true"  //是否支持其他应用调用启动该activity,true为是。

还加入了关于蓝牙的两个权限,BLUETOOTH和BLUETOOTH_ADMIN,前者用于允许与已经配对的蓝牙设备进行连接主要是配对后的权限,后者用于允许发现和配对蓝牙设备,主要是配对前的权限。

好了,属性配置就介绍到这儿了,接下来要真正开始蓝牙模块的学习了,首先明确模块的布局,蓝牙模块的功能,蓝牙实现的有:开启蓝牙,蓝牙重命名,蓝牙检测性及检测时间设置,扫描附近可用蓝牙设备,加载已经配对的蓝牙设备,与设备配对,连接,通信。


ONE,蓝牙布局实现

public final class BluetoothSettings extends DeviceListPreferenceFragment {


.............
        @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
        addPreferencesFromResource(R.xml.bluetooth_settings);
      }

............
}


      1>,可以看出BluetoothSettings属于PreferenceFragment,所要加载的布局文件为Bluetooth_settings.xml文件。以下是布局文件代码,总共四行,节点为PreferenceScreen,代表显示整个屏幕,内部可嵌套不同类型的标签,在这里内部未有任何标签,是在代码中动态添加的不同种类的布局。



   

    2>,展示两张蓝牙开启和关闭时布局示意图


       Android4.42-Settings源码分析之蓝牙模块Bluetooth(上)_第1张图片                        Android4.42-Settings源码分析之蓝牙模块Bluetooth(上)_第2张图片


  • 圈1:ActionBar顶部导航栏,显示title,以及蓝牙开关,开关的添加代码在addPreferencesForActivity方法中,

@Override
    void addPreferencesForActivity() {
        Activity activity = getActivity();

        //创建蓝牙开关控件
        Switch actionBarSwitch = new Switch(activity);

        if (activity instanceof PreferenceActivity) {
            PreferenceActivity preferenceActivity = (PreferenceActivity) activity;
            if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) {
                final int padding = activity.getResources().getDimensionPixelSize(
                        R.dimen.action_bar_switch_padding);
                actionBarSwitch.setPaddingRelative(0, 0, padding, 0);
                //用来进行顶部导航栏的布局,顶部导航栏左边显示图标和title
               activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
                        ActionBar.DISPLAY_SHOW_CUSTOM);
                //顶部导航栏右边显示开关,控件宽高自适应,垂直居中
               activity.getActionBar().setCustomView(actionBarSwitch, new ActionBar.LayoutParams(
                        ActionBar.LayoutParams.WRAP_CONTENT,
                        ActionBar.LayoutParams.WRAP_CONTENT,
                        Gravity.CENTER_VERTICAL | Gravity.END));
            }
        }
         //将开关控件传给BluetoothEnabler.用来更新蓝牙的开关状态
        mBluetoothEnabler = new BluetoothEnabler(activity, actionBarSwitch);
         //告知options menu ,fragment要添加菜单项
        setHasOptionsMenu(true);
    }


那么开关控件的初始状态是如何获取的呢??进入到BluetoothEnabler.java类中可以发现,在该类的resume方法中对该switch有一个设置

Android4.42-Settings源码分析之蓝牙模块Bluetooth(上)_第3张图片


其中handleStateChanged方法就是传入当前蓝牙的状态,并对开关的状态进行设置。

在手机恢复出厂设置后可以看到开关状态的默认值,该默认值对应的是def_bluetooth_on,在开机过程中会将该默认值对应的boolean值通过蓝牙服务BluetoothManagerService保存起来,并通过本地的蓝牙适配器获取到当前蓝牙状态传给switch开关。

所以如果你想修改蓝牙默认开关可以在framework/base/packages/SettingsProvider/res/values/default.xml中修改对应字段。

  • 圈2:ActionBar底部栏,可进行蓝牙设备的搜索,检测时间,已配对设备列表等一些除了配对之外的设置,Actionbar的相关布局在onCreateOptionsMenu方法中,利用如下代码可自定义actionbar

               menu.add(groupId, itemId, order, title)
               .setEnabled(enabled)
                .setShowAsAction(actionEnum);
        

         group_id:int 型数值,代表组的意思

         item_id:  int 型数值,每个菜单选项的唯一标识

         order_id:int 型数值,菜单显示的顺序,如果为0表示按add顺序显示

        title:        charsequence型数字,菜单item的title

       setEnabled(enable):用来设置是否可点击

       setShowAsAction(actionEnum) : 用来设置屏幕宽度不同时item的显示,actionEnum有以下几个取值。

  • 圈3:蓝牙未开启时preferencescreen没有任何类别,listview的emptyview

         getListView().setEmptyView(mEmptyView);

  • 圈4:本机蓝牙设备的相关设置,包括本机蓝牙名称,蓝牙对附近可用设备的可见性,蓝牙对已经配对设备的可见性,当检测到蓝牙开启时会添加一个本机蓝牙信息的Preference,在方法updateContent中完成添加或者移除,添加代码如下:

 if (mMyDevicePreference == null) {
               //创建一个Preference
               mMyDevicePreference = new Preference(getActivity());
                }
                 //下面代码用来设置Preference的标题,图标,是否可点击
              mMyDevicePreference.setTitle(mLocalAdapter.getName());
				
                if (getResources().getBoolean(com.android.internal.R.bool.config_voice_capable)) {
                  //如果是手机则显示手机的图标
                    mMyDevicePreference.setIcon(R.drawable.ic_bt_cellphone);    // for phones
                } else {
                 //如果不是手机诸如笔记本电脑,带有蓝牙模块的单片机等,则显示电脑的图标
                  mMyDevicePreference.setIcon(R.drawable.ic_bt_laptop);   // for tablets, etc.
                }
                //是否将该Preference的值写入SharedPreference文件中,true代表写入
                mMyDevicePreference.setPersistent(false);
                //是否可点击
               mMyDevicePreference.setEnabled(true);
              //添加一个Preference
               preferenceScreen.addPreference(mMyDevicePreference);

                

                    preferencescreen添加或者移除的代码如下:

//添加一个种类的Preference
getPreferenceScreen().addPreference(mAvailableDevicesCategory);
.,....
//移除一个Preference
preferenceScreen.removePreference(mPairedDevicesCategory);
.....
//移除所有
Preference preferenceScreen.removeAll();

 
  

  • 圈5:已配对设备列表mPairedDevicesCategory
  • 圈6:附近可用设备列表mAvailableDevicesCategory

总的来说,蓝牙布局的实现借助的是actionbar+Preference,均是在代码中动态的添加布局,Actionbar的添加操作在方法addPreferencesForActivity和onCreateOptionsMenu中实 现。不同Category的Preference的添加和修改与蓝牙开关状态、是否有已经配对的蓝牙设备以及附近是否有可用的蓝牙设备。

蓝牙界面的布局暂且介绍到这儿,有问题的可博文下留言,我再进行补充。

TWO,蓝牙模块方法简介

蓝牙模块打开后执行流程getHelpResource()---->addPreferencesForActivity()--->onCreateView()--->initDevicePreference()--->onAcitivityCreated()--->onResume()-->initDevicePreference()--->onCreateOptionsMenu()。

先介绍一下覆写的方法的作用

1>,getResource()方法,定义在SettingPreferenceFragment.java类中,默认返回的是0,方法的解释是如果想要在菜单栏上显示help item,可以覆写该方法,用于一些说明(Specified in product overlays)。

2>,addPreferencesForActivity()方法,用于添加actionbar上的switch,代码见蓝牙布局部分

3>,onCreateView()方法,fragment的生命周期方法,用于加载xml布局

@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
				Bundle savedInstanceState) {
		addPreferencesFromResource(R.xml.bluetooth_settings);
		View v = inflater.inflate(R.layout.add_preference_list_fragment,null);
		mEmptyView = (TextView) v.findViewById(R.id.add_empty);
}


4>,initDevicePreference()方法,获取到已经配对的蓝牙设备,设置监听事件

@Override
    void initDevicePreference(BluetoothDevicePreference preference) {
        CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
        if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
            // Only paired device have an associated advanced settings screen
           //如果设备已经配对,则添加监听事件
           preference.setOnSettingsClickListener(mDeviceProfilesListener);
        }
    }

5>,onDevicePreferenceClick()方法,远程蓝牙设备的点击事件

 @Override
    void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
       //停止扫描 
      mLocalAdapter.stopScanning();
       //调用父类的点击事件
       super.onDevicePreferenceClick(btPreference);
    }

6>,onBluetoothStateChanged()方法,蓝牙开关状态改变时监听

7>,onScanningStateChanged()方法,监听扫描可用蓝牙设备时扫描的状态改变,开启扫描,正在扫描,扫描结束,并更新进度条


THREE,蓝牙功能实现流程

功能模块这块儿主要分析一下实现的流程,代码为辅,若在看源码时代码有什么问题,可在博文下咨询

1>,蓝牙开关switch相关,

蓝牙开关涉及到本地蓝牙状态的更改以及用户点击switch更改蓝牙状态,当本地蓝牙状态发生改变时需要更新switch的状态,当switch的状态发生改变时需要更新本地的蓝牙状态。这就涉及到了,注册广播监听本地蓝牙状态,为switch注册监听器监听switch的更改,以及对switch状态进行设置的方法。

首先执行addPreferencesForActivity加载switch,在该方法中构造BluetoothEnabler对象,对switch的状态进行初始化以及状态改变的监听。

接下来对BluetoothEnabler进行分析,先看一下BluetoothEnabler的构造方法

 public BluetoothEnabler(Context context, Switch switch_) {
        mContext = context;
        mSwitch = switch_;
        mValidListener = false;
       
      //首先判断是否支持蓝牙
      LocalBluetoothManager manager = LocalBluetoothManager.getInstance(context);
        if (manager == null) {
            // Bluetooth is not supported不支持蓝牙此时蓝牙本地适配器为null,开关状态为未选中
          mLocalAdapter = null;
            mSwitch.setEnabled(false);
        } else {
            //如果支持蓝牙获取到本地蓝牙适配器adapter,
           mLocalAdapter = manager.getBluetoothAdapter();
        }
           //蓝牙开关状态的过滤器
         mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
		
    }
紧接着在resume()中进行蓝牙开关状态的设置

public void resume() {
        if (mLocalAdapter == null) {
            //如果蓝牙适配器为空,则将resume方法返回,不进行resume方法中的剩余操作
           mSwitch.setEnabled(false);
            return;
        }

        // Bluetooth state is not sticky, so set it manually
       //必须手动的去监听蓝牙状态的改变
        //根据本地蓝牙适配器获取到此时蓝牙的状态,对switch进行设置
       handleStateChanged(mLocalAdapter.getBluetoothState());
         
        //注册广播监听蓝牙状态的改变
        mContext.registerReceiver(mReceiver, mIntentFilter);
        //为switch设置监听事件
       mSwitch.setOnCheckedChangeListener(this);
        mValidListener = true;
    }

在resume方法中做了三件事,

i>,根据本地蓝牙适配器获取到此时的蓝牙状态对switch进行设置handleStateChanged(state)方法代码很简单,不再赘述

ii>,注册广播监听蓝牙状态-----当系统蓝牙状态发生改变时需要更新switch状态,广播接收器中的代码如下

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Broadcast receiver is always running on the UI thread here,
            // so we don't need consider thread synchronization.
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
             //针对不同的蓝牙状态对switch进行设置
            handleStateChanged(state);
        }
    };

iii>,为switch设置监听事件,当switch发生改变时,需要对系统的蓝牙状态进行行改变。系统的蓝牙开关状态发生改变时,会发送状态改变的广播,对switch进行更改

 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 
        // shouldn't setBluetoothEnabled(true) in airplane mode.
      //飞行模式下蓝牙不可用
     if (mLocalAdapter != null) {
            if (isChecked && WifiSettings.needPrompt(mContext)) {
                return;
            }
          //当switch开关状态发生改变时,对系统本地蓝牙状态进行设置
          mLocalAdapter.setBluetoothEnabled(isChecked);
        }
    //当switch状态进行改变时,让其不可点击
     mSwitch.setEnabled(false);
    }

接下来看看对本地蓝牙适配器更改的方法

public void setBluetoothEnabled(boolean enabled) {
       //根据switch的enable来开启或者关闭蓝牙,success返回执行结果
        boolean success = enabled
                ? mAdapter.enable()
                : mAdapter.disable();
        isPairing = false;
        if (success) {
           //如果系统蓝牙开启或者关闭操作成功,将状态更新
            setBluetoothStateInt(enabled
                ? BluetoothAdapter.STATE_TURNING_ON
                : BluetoothAdapter.STATE_TURNING_OFF);
        } else {
           //如果系统蓝牙没有开启或者关闭成功,则将蓝牙状态进行更新保存为当前系统蓝牙的状态
            syncBluetoothState();
        }
    }
BluetoothAdapter的enable方法用于开启蓝牙,disable用于关闭蓝牙

2>,本机蓝牙设置,包括可检测性、蓝牙名称、可检测时间。

i>,加载本机蓝牙相关信息

在updateContent方法中进行动态的添加preference(单一控件,类似checkbox)或者preferencecategory(组合控件,类似linearlayout)。本机蓝牙的信息添加的是一个preference

if (mMyDevicePreference == null) {
                    mMyDevicePreference = new Preference(getActivity());
                }
               //设置preference的title标题,显示的是蓝牙名称
               mMyDevicePreference.setTitle(mLocalAdapter.getName());
		//如果是手机,图标设置为手机的图标,如果是平板电脑或其他则设置为电脑图标		
                if (getResources().getBoolean(com.android.internal.R.bool.config_voice_capable)) {
                    mMyDevicePreference.setIcon(R.drawable.ic_bt_cellphone);    // for phones
                } else {
                    mMyDevicePreference.setIcon(R.drawable.ic_bt_laptop);   // for tablets, etc.
                }
                //是否将该preference的信息保存在sharedPreference中
               mMyDevicePreference.setPersistent(false);
                //设置preference可点击
               mMyDevicePreference.setEnabled(true);
                //添加mMyDevicePreference
                preferenceScreen.addPreference(mMyDevicePreference);

                if (!isRestrictedAndNotPinProtected()) {
                    if (mDiscoverableEnabler == null) {
                        mDiscoverableEnabler = new BluetoothDiscoverableEnabler(getActivity(),
                                mLocalAdapter, mMyDevicePreference);
                        //进行蓝牙可检测性的设置
                         mDiscoverableEnabler.resume();
                        LocalBluetoothManager.getInstance(getActivity()).setDiscoverableEnabler(
                                mDiscoverableEnabler);
                    }
                }
                  
                // Paired devices category
                //加载已经配对的设备列表
              if (mPairedDevicesCategory == null) {
                    mPairedDevicesCategory = new PreferenceCategory(getActivity());
                } else {
                    mPairedDevicesCategory.removeAll();
                }
                addDeviceCategory(mPairedDevicesCategory,
                        R.string.bluetooth_preference_paired_devices,
                        BluetoothDeviceFilter.BONDED_DEVICE_FILTER);
                //获取到已经配对的设备的数量,关系到mMyDevicePreference的summary显示的文本
               int numberOfPairedDevices = mPairedDevicesCategory.getPreferenceCount();
                if (mDiscoverableEnabler != null) {
                         //根据已配对的数量对显示的summary进行处理
                         mDiscoverableEnabler.setNumberOfPairedDevices(numberOfPairedDevices);
                }
 

ii>,修改蓝牙名称

修改蓝牙名称的按钮在菜单栏中id为MENU_ID_RENAME_DEVICE,过程是修改后将蓝牙名称赋给系统的蓝牙适配器,系统蓝牙适配发送广播通知蓝牙名称已经修改,在接受到蓝牙名称修改后的广播后更新preference的title。代码流程如下

 public boolean onOptionsItemSelected(MenuItem item) {
          ........
            case MENU_ID_RENAME_DEVICE:
              //弹出修改的对话框,在点击确定时会调用修改蓝牙名称的方法
             
                new BluetoothNameDialogFragment().show(
                        getFragmentManager(), "rename device");
                return true;
                ......
}

当蓝牙名称发生变化后,会发送广播通知蓝牙名称已变,对preference进行更新。在此进行强调,只要是对对话框中的编辑框进行了编辑,不论内容是否修改(比如删除之后又添加上一模一样的),均会发送蓝牙名称已经更改的广播。至此,蓝牙名称的修改已经结束

iii>,蓝牙可检测性的修改

先普及一个知识有助于理解蓝牙的可检测性,BluetoothAdapter的getScanMode有三个值,它们的含义分别是

SCAN_MODE_NONE,int型值,大小为20,表示对任何设备不可见,且无法进行扫描功能

SCAN_MODE_CONNECTABLE,int型值,大小为21,表示只对已经配对的设备可见,可以扫描其他设备

SCAN_MODE_CONNECTABLE_DISCOVERABLE,int型值,大小为23,表示对附近所有设备可见,可以扫描其他设备。

蓝牙的可检测性由本地蓝牙的扫描模式BluetoothAdapter的getScanMode()来决定,所以接下来首先将蓝牙的可检测性显示在mMyDevicePreference的summary副标题处,然后副标题的更新位于类BluetoothDiscoverableEnabler中,在该类的resume方法中首先需要注册广播监听本地蓝牙扫描模式的改变

 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //监听蓝牙的扫描模式的改变
             if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
                int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
                        BluetoothAdapter.ERROR);
                if (mode != BluetoothAdapter.ERROR) {
                  //如果扫描模式发生了改变且没有发生错误,就去更新副标题
                   handleModeChanged(mode);
                }
            }
        }
    };

更新副标题的方法如下,因为分三种模式,所以副标题也有三种情况

void handleModeChanged(int mode) {
        //对附近所有设备可见,且可扫描
        if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            //将标志位置为true,与该preference的点击事件有关
            mDiscoverable = true;
          //根据时间更新副标题,此时副标题显示的是对附近所有设备可见以及可见时长
           updateCountdownSummary();
        } else {
           
          mDiscoverable = false;
            //更新副标题,如果已配对设备列表为空,则为对所有设备不可见,如果已配对设备列表不为空,则为对已配对设备可见
            setSummaryNotDiscoverable();
        }
    }

然后为preference添加一个点击事件,当点击preference时将标志位取反,并且更新preference的summary以及蓝牙的扫描模式

public boolean onPreferenceClick(Preference preference) {
        mDiscoverable = !mDiscoverable;
        setEnabled(mDiscoverable);
        return true;
    }

在更新summary的时候涉及到对可检测性时间的更新,说一下实现逻辑不贴代码了,有需要的再问吧

首先明确可检测性事件,然后在开启限时的可检测性后再更新summary的方法中开启一个线程,该线程中再次调用该更新summary的方法,在更新summary中的方法中会对时间进行判断,如果时间结束了,就退出该方法。

3>,已配对设备列表

见下一篇  Android4.42-Setting源码分析之蓝牙模块Bluetooth(下)



 
  




你可能感兴趣的:(Android-蓝牙BT版块)