android 蓝牙分析(一)

最近公司想要使蓝牙a2dp source和a2dp sink动态切换。

于是决定进行相应的源码调整。现在将一些分析结果整理一下

因为从来没有android 蓝牙的工作经验,所以先从android的蓝牙架构开始

注意:本次分析使用了msm8996 android 8.1 平台

一,android蓝牙架构

查看android 官网,可以获得架构相关的知识,如下图

android蓝牙结构图 android蓝牙架构_Bluetooth UI

二,从上到下的源码分析第一步Settings 应用

从架构图中,先找到蓝牙架构的应用app,然后逐渐往下。首先是Settings应用中的蓝牙。

从Settings的AndroidManifest.xml文件中,可以看到下面的蓝牙入口代码:

<activity android:name="Settings$BluetoothSettingsActivity"
                android:label="@string/bluetooth_settings_title"
                android:icon="@drawable/ic_settings_bluetooth"
                android:configChanges="orientation|keyboardHidden|screenSize"
                android:taskAffinity="">
            <intent-filter android:priority="1">
                <action android:name="android.settings.BLUETOOTH_SETTINGS" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.VOICE_LAUNCH" />
                <category android:name="com.android.settings.SHORTCUT" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                android:value="com.android.settings.bluetooth.BluetoothSettings" />
        </activity>

上面的intent-filter标签,可以通过intent唤出蓝牙设置界面。meta-data标签,由Settingslib包进行解析。表示,该设置界面使用的是下面的Fragment

com.android.settings.bluetooth.BluetoothSettings

上面的作用机制和原理,应该属于Settings应用,此处不做过多解释。

进入BluetoothSettings.java文件中,查看整个蓝牙的应用。

BluetooghSettings.java为Fragment,查看这个可以查看它的几个关键点,有以下几个:

  1. onActivityCreated______表示当前activity创建之后的动作
  2. onStart_____表示Fragment的开始阶段应该做的动作
  3. 相应的相对的关键点有:onDestroyView(),onStop().
  4. 因为是讨论蓝牙的问题,此处是Settings的细节,不再做过多的赘述

如果查看Fragment的生命周期,会发现,下面的生命周期

onAttach();
onCreate();
onCreateView();
onActivityCreated();
onStart();
onResume();
onPause();
onStop();
onDestroyView();
onDestroy();
onDetach();

BluetoothSettings.java中没有实现的生命关键节点,由它的父类帮忙完成了。等以后遇到要改Settings的时候,再来写篇文章详细记录。此处不再过多讨论。直接接入BluetoothSettings.java的onActivityCreated()里面看看

@Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        /* Don't auto start scan if screen reconstructs due to frozen screen*/
        //mInitialScanStarted = (savedInstanceState != null);
        //mInitiateDiscoverable = true;

        final SettingsActivity activity = (SettingsActivity) getActivity();
        mSwitchBar = activity.getSwitchBar();

        mBluetoothEnabler = new BluetoothEnabler(activity, new SwitchBarController(mSwitchBar),
                mMetricsFeatureProvider, Utils.getLocalBtManager(activity),
                MetricsEvent.ACTION_BLUETOOTH_TOGGLE);
        mBluetoothEnabler.setupSwitchController();
        if (mLocalAdapter != null) {
            mAlwaysDiscoverable = new AlwaysDiscoverable(getContext(), mLocalAdapter);
        }
    }

第一步:获取Activity,在Settings应用中,将很多重复的操作,都集中在了父类SettingsActivity中了。此处蓝牙使用的Activity为

public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }

第二步:获取Activity的SwitchBar. SettingsActivity会在创建的使用,使用一些默认的界面,这些界面就包含一个称为SwitchBar的组件。使用者只要注册一个SwitchBar.OnSwitchChangeListener监听器,就能根据SwitchBar的状态,做相应的改变了。比如在蓝牙界面,监听SwitchBar的状态打开或关闭蓝牙,在wifi界面监听SwitchBar的状态打开或者关闭蓝牙。

第三步:创建一个称为BluetoothEnable的对象,从它的参数我们可以知道,这个对象是根据SwitchBar的状态,做相应的操作,可以认为是SwitchBar的蓝牙开关封装对象(命名说明:蓝牙表示了,是跟蓝牙相关;开关,表示了是对蓝牙打开关闭的一种逻辑表示;封装表示了,对switchbar组件的一种再次包装)

第四步:蓝牙开关封装对象的初始化

第五步:创建AlwaysDiscoverable对象,用于控制蓝牙可被发现。

那么接下来,解决一个疑问:上面的代码好像,并没有界面相关的操作唉,设置中蓝牙的打开和关闭的默认界面是什么?

从Fragment的声明周期可以知道,Fragment在onActivityCreated之前还有一个onAttach();onCreate();onCreateView();几乎上个关键节点可用。

onAttach();//当Fragment第一次被加载到它的上下文时,该函数被调用
onCreate();//Fragment这个逻辑对象创建的关键节点,可以在这个函数中,初始化Fragment需要使用的一些对象。
//思考:如果我将同一个Fragment从一个Activity放到另外一个Activity,是否意味着,这个Fragment的onCreate()会被再次调用????
onCreateView();//嘿嘿,创建Fragment使用的UI的关键位置了。

按照这个步骤,分别翻越源码。从onCreateView()开始。

BluetoothSettings.java的onCreateView()的地方有:DashboardFragment.java,PreferenceFragment.java

其中DashboardFragment.java为Settings应用中自己实现的Fragment,他调用的是父类的同名方法。因此实现依然是在PreferenceFragment.java中。

Settings应用中使用的PreferenceFragment来自于android.support.v14.preference包。

接下来简单的看看PreferenceFragment.java中是怎么创建根UI的。

定位到onCreateView代码如下:

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        TypedArray a = mStyledContext.obtainStyledAttributes(null,
                R.styleable.PreferenceFragment,
                TypedArrayUtils.getAttr(mStyledContext,
                        android.support.v7.preference.R.attr.preferenceFragmentStyle,
                        AndroidResources.ANDROID_R_PREFERENCE_FRAGMENT_STYLE),
                0);

        mLayoutResId = a.getResourceId(R.styleable.PreferenceFragment_android_layout, mLayoutResId);

        final Drawable divider = a.getDrawable(R.styleable.PreferenceFragment_android_divider);
        final int dividerHeight = a.getDimensionPixelSize(
                R.styleable.PreferenceFragment_android_dividerHeight, -1);
        final boolean allowDividerAfterLastItem = a.getBoolean(
                R.styleable.PreferenceFragment_allowDividerAfterLastItem, true);
        a.recycle();

        // Need to theme the inflater to pick up the preferenceFragmentListStyle
        final TypedValue tv = new TypedValue();
        getActivity().getTheme().resolveAttribute(
                android.support.v7.preference.R.attr.preferenceTheme, tv, true);
        final int theme = tv.resourceId;

        final Context themedContext = new ContextThemeWrapper(inflater.getContext(), theme);
        final LayoutInflater themedInflater = inflater.cloneInContext(themedContext);

        final View view = themedInflater.inflate(mLayoutResId, container, false);

        final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER);
        if (!(rawListContainer instanceof ViewGroup)) {
            throw new RuntimeException("Content has view with id attribute "
                    + "'android.R.id.list_container' that is not a ViewGroup class");
        }

        final ViewGroup listContainer = (ViewGroup) rawListContainer;

        final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
                savedInstanceState);
        if (listView == null) {
            throw new RuntimeException("Could not create RecyclerView");
        }

        mList = listView;

        listView.addItemDecoration(mDividerDecoration);
        setDivider(divider);
        if (dividerHeight != -1) {
            setDividerHeight(dividerHeight);
        }
        mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem);

        listContainer.addView(mList);
        mHandler.post(mRequestFocus);

        return view;
    }

应用开发者应该非常熟悉的一些常见操作,默认加载的界面为:preference_list_fragment.xml文件。此处就是PreferenceFragment的实现细节了,不再次讨论,我们的目标为蓝牙。

使用UI工具,可以看到,当关闭蓝牙时,Fragment里面的“@android:id/empty”被设置为:“开启蓝牙后,你的设备可以与附近的其他蓝牙设备通信”

可是我们的设置界面,还有一个蓝牙的开关,即上文提到的SwitchBar.显然蓝牙设置界面的UI还有Activity的UI在。那么进入Activity的UI的创建过程,以解决上文的疑问——蓝牙的界面是怎么形成的?

BluetoothSettings使用的Activity为

public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }

真正的实现在SettingsActivity.java中。这是一个Activity,那么直接定位到onCreate函数中,如下:

@Override
    protected void onCreate(Bundle savedState) {
        super.onCreate(savedState);

        //省略一部分
        mIsShowingDashboard = className.equals(Settings.class.getName());
        //省略一部分

        setContentView(mIsShowingDashboard ?
                R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
        //省略一部分
        mContent = findViewById(R.id.main_content);

上面的代码只保留了,跟ui相关的部分,用于说明Settings中各个UI的关系。细节的话,建议参考Settings的源码分析,有空了我也给整一个Settings的源码分析。

setContentView根据mIsShowingDashboard来选择不同的UI界面。mIsShowingDashboard表示的意义是:是否是Settings的最顶层界面,这个界面被称作:DashBoard(暂且译做设置主界面)。

如果不是DashBoard界面,则使用R.layout.settings_main_prefs.

使用UI工具,依然可以看到,最上层的被称作actionbar的区域,并没有这Activity使用的xml中。那么接下来就是查看,actionbar是怎么被加载出来的了。

首先从SettingsActivity的父类中翻阅,他的父类为SettingsDrawerActivity。在

./frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java

进入这个文件的onCreate中查看。如下:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //省略部分
        super.setContentView(R.layout.settings_with_drawer);
        mContentHeaderContainer = (FrameLayout) findViewById(R.id.content_header_container);

        Toolbar toolbar = (Toolbar) findViewById(R.id.action_bar);
        if (theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) {
            toolbar.setVisibility(View.GONE);
            return;
        }
        setActionBar(toolbar);

        if (DEBUG_TIMING) {
            Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }

上面的代码,将settings_with_drawer中的Toolbar,设置为当前界面的actionbar。

settings_with_drawer.xml.继续查看该文件。

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- The main content view -->
    <LinearLayout
        android:id="@+id/content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:fitsSystemWindows="true">
        <Toolbar
            android:id="@+id/action_bar"
            style="?android:attr/actionBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="?android:attr/actionBarTheme"
            android:navigationContentDescription="@*android:string/action_bar_up_description"/>
        <FrameLayout
            android:id="@+id/content_header_container"
            style="?android:attr/actionBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        <FrameLayout
            android:id="@+id/content_frame"
            android:layout_width="match_parent"
            android:layout_height="fill_parent"
            android:background="?android:attr/windowBackground" />
    </LinearLayout>
</android.support.v4.widget.DrawerLayout>

Nice!!!,上面的Toolbar,即为整个界面的action bar区域。

现在回到最开始的地方PreferenceFragment的内容,是在什么时候,被设置的,即“开启蓝牙后,你的设备可以与附近的其他蓝牙设备通信”是在什么时候设置的。

按照逻辑划分——谁要使用,谁来负责这里面的内容。这部分的内容设置,一定会落在BluetootSettings,或者它的组成类中。

BluetoothSettings的onActivityCreated已经查看完毕。接下来查看onStart()如下:

@Override
    public void onStart() {

        // resume BluetoothEnabler before calling super.onStart() so we don't get
        // any onDeviceAdded() callbacks before setting up view in updateContent()
        if (mBluetoothEnabler != null) {
            mBluetoothEnabler.resume(getActivity());
        }
        super.onStart();
        // Always show paired devices regardless whether user-friendly name exists
        mShowDevicesWithoutNames = true;
        if (isUiRestricted()) {
            getPreferenceScreen().removeAll();
            if (!isUiRestrictedByOnlyAdmin()) {
                getEmptyTextView().setText(R.string.bluetooth_empty_list_user_restricted);
            }
            return;
        }

        if (mLocalAdapter != null) {
            updateContent(mLocalAdapter.getBluetoothState());
        }
    }

第一步:对BluetoothEnabler进行一个调用,作用在后面介绍,主要就是初始化SwitchBar的各种状态,并监听

第二步:是否是限制的UI界面,如果是则显示其他的内容,并不进一步操作。

第三步:根据蓝牙适配器的状态,跟新内容。

进入updateContent(),如下:

private void updateContent(int bluetoothState) {
        int messageId = 0;

        switch (bluetoothState) {
            
            //省略部分
            case BluetoothAdapter.STATE_OFF:
                setOffMessage();
                if (isUiRestricted()) {
                    messageId = R.string.bluetooth_empty_list_user_restricted;
                }
                break;
            //省略部分
          
        }

        displayEmptyMessage(true);
        if (messageId != 0) {
            getEmptyTextView().setText(messageId);
        }
    }

加入蓝牙默认为关闭状态,那么就,进入setOffMessage().这回好了,几乎可以肯定,setOffMessage就是我们要寻找的最终位置了,后续就不再继续了,毕竟本篇文章的主题是蓝牙。而不是Settings。

自此,整个蓝牙界面的默认UI界面,有了一个大概的认识。

总结如下:

  1. actionbar的区域来自于BluetoothSettingsActivity的父类SettingsDrawerActivity的onCreate函数里面的设置。
  2. 带有SwitchBar的区域来自于BluetoothSettingsActivity的父类SettingsActivity的onCreate函数的设置。
  3. 剩下的区域,为BluetoothSettings的父类PreferenceFragment的默认界面。

但是蓝牙界面的形成,还有一半,即当点击SwitchBar时,蓝牙的界面会更改,那么它的整个界面是怎么形成的呢??

由上文的蓝牙开关封装对象即BluetoothEnabler对象,可以作为入手点。也可以将SwitchBar.OnSwitchChangeListener作为入手点。

我现在先看到了BluetoothEnabler对象,那么就以它作为入手点吧。它在onActivityCreated中被创建,在onStart中被调用resume。

先看看创建,如下

public BluetoothEnabler(Context context, SwitchWidgetController switchWidget,
            MetricsFeatureProvider metricsFeatureProvider, LocalBluetoothManager manager,
            int metricsEvent, RestrictionUtils restrictionUtils) {
        mContext = context;
        mMetricsFeatureProvider = metricsFeatureProvider;
        mSwitchWidget = switchWidget;
        mSwitch = mSwitchWidget.getSwitch();
        mSwitchWidget.setListener(this);
        mValidListener = false;
        mMetricsEvent = metricsEvent;

        if (manager == null) {
            // Bluetooth is not supported
            mLocalAdapter = null;
            mSwitchWidget.setEnabled(false);
        } else {
            mLocalAdapter = manager.getBluetoothAdapter();
        }
        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        mRestrictionUtils = restrictionUtils;
    }

上面全都是对象的赋值,实在没有必要多少,需要注意一下几点:

  1. mSwitchWidget,为SwitchBar的封装,他管理SwitchBar的显示与隐藏,并监听SwitchBar的状态,并把SwitchBar的状态转发给
    BluetoothEnabler对象。(阅读是,可注意三个对象的接口对象,画图更好理解,因为太懒了,不想画图)
  2. IntentFilter对象监听的是适配器的状态改变。

接下来,看看resume函数

public void resume(Context context) {
        if (mContext != context) {
            mContext = context;
        }

        final boolean restricted = maybeEnforceRestrictions();

        if (mLocalAdapter == null) {
            mSwitchWidget.setEnabled(false);
            return;
        }

        // Bluetooth state is not sticky, so set it manually
        if (!restricted) {
            handleStateChanged(mLocalAdapter.getBluetoothState());
        }

        mSwitchWidget.startListening();
        mContext.registerReceiver(mReceiver, mIntentFilter);
        mValidListener = true;
    }

resume就做了两件事:

  1. 根据蓝牙适配器的状态,手动更新一下SwitchBar的状态
  2. 注册一个蓝牙状态的广播,用于监听,蓝牙状态的改变。

当我们点击SwitchBar时,会将状态改变传递给mSwitchWidget对象,这个对象,将状态传递给BluetoothEnabler
前者传递状态使用的接口为:SwitchBar.OnSwitchChangeListener。后两者传递状态使用的是:SwitchWidgetController.OnSwitchChangeListener

最终,会调用BluetoothEnabler的onSwitchToggled函数,如下

@Override
    public boolean onSwitchToggled(boolean isChecked) {
        if (maybeEnforceRestrictions()) {
            return true;
        }

        // Show toast message if Bluetooth is not allowed in airplane mode
        if (isChecked &&
                !WirelessUtils.isRadioAllowed(mContext, Settings.Global.RADIO_BLUETOOTH)) {
            Toast.makeText(mContext, R.string.wifi_in_airplane_mode, Toast.LENGTH_SHORT).show();
            // Reset switch to off
            mSwitch.setChecked(false);
            return false;
        }

        mMetricsFeatureProvider.action(mContext, mMetricsEvent, isChecked);

        if (mLocalAdapter != null) {
            boolean status = mLocalAdapter.setBluetoothEnabled(isChecked);
            // If we cannot toggle it ON then reset the UI assets:
            // a) The switch should be OFF but it should still be togglable (enabled = True)
            // b) The switch bar should have OFF text.
            if (isChecked && !status) {
                mSwitch.setChecked(false);
                mSwitch.setEnabled(true);
                mSwitchWidget.updateTitle(false);
                return false;
            }
        }
        mSwitchWidget.setEnabled(false);
        return true;
    }

这个函数也非常的简单,做了如下的工作:

  1. 判断飞行模式是否打开,如果打开了,将会弹出一个Toast,并不允许打开蓝牙。
  2. 调用蓝牙适配器,并设置蓝牙状态。

一旦蓝牙适配器做出了各种状态变化,将会被BluetoothEnabler.mReceiver对象接受到,并根据实际的蓝牙状态,
更新SwitchBar的状态。

可知,BluetoothEnabler里面并没有我们要找的UI相关的东西。但是它却操作了蓝牙适配器的打开和关闭,按照BluetoothEnabler
接受蓝牙状态的方法。可以猜测,BluetoothSettings.java中也是使用同样的方法来接收蓝牙状态的改变,并据此来更新UI的。
如果不是,那么也有可能是BluetoothSettings的组合类中使用了类似的方法,直接全局搜索,BluetoothAdapter.ACTION_STATE_CHANGED。

从搜索结果中,可以看到Settings使用的SettingsLib库有这个的监听,监听的位置为:BluetoothEventManager.java
位置如下:

./frameworks/base/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java

查看代码如下:

BluetoothEventManager(LocalBluetoothAdapter adapter,
            CachedBluetoothDeviceManager deviceManager, Context context) {
        //省略部分
        // Bluetooth on/off broadcasts
        addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
        // Generic connected/not broadcast
        //省略部分
    }

可以看到,蓝牙状态的改变,会交由AdapterStateChangedHandler对象来处理。

注意:在此处直接给出了蓝牙状态会交给AdapterStateChangedHandler对象处理,这中间的细节没有过多的交代,
下面简述之:addHandler函数将一个action和Handler对象进行关联(关联的形式是一个HashMap),并监听这个action
一旦,广播收到某一个action,就从HashMap中直接取出Handler,并调用Handler的onReceive方法。交由Handler来处理

接下来看看AdapterStateChangedHandler的处理过程,如下:

private class AdapterStateChangedHandler implements Handler {
        public void onReceive(Context context, Intent intent,
                BluetoothDevice device) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
                                    BluetoothAdapter.ERROR);
            // Reregister Profile Broadcast Receiver as part of TURN OFF
            if (state == BluetoothAdapter.STATE_OFF)
            {
                context.unregisterReceiver(mProfileBroadcastReceiver);
                registerProfileIntentReceiver();
            }
            // update local profiles and get paired devices
            mLocalAdapter.setBluetoothStateInt(state);
            // send callback to update UI and possibly start scanning
            synchronized (mCallbacks) {
                for (BluetoothCallback callback : mCallbacks) {
                    callback.onBluetoothStateChanged(state);
                }
            }
            // Inform CachedDeviceManager that the adapter state has changed
            mDeviceManager.onBluetoothStateChanged(state);
        }
    }
  1. 如果是关闭了蓝牙,重新注册一下mProfileBroadcastReceiver
  2. Android的标准API 使用的蓝牙适配器对象为BluetoothAdapter.而Settings应用中对这个适配器做了一层封装
    名字叫做LocalBluetoothAdapter.该步骤就是更新这个LocalBluetoothAdapter的蓝牙状态
  3. 调用BluetoothCallback回调,通知蓝牙的状态已经改变
  4. 通知CachedDeviceManager,蓝牙状态改变。Settings应用会将已经配对的设备进行缓冲,并创建CachedDeviceManager对象来管理

那么接下来可以90%的猜测,BluetoothSettings使用了BluetoothEventManager的BluetoothCallback。

在BluetoothSettings和其父类中进行搜索BluetoothCallback。可以发现,BluetoothSettings
的父类DeviceListPreferenceFragment,在onStart中讲自己作为了BluetoothCallback并注册给
BluetoothEventManager

BluetoothEventManager中的AdapterStateChangedHandler,将会在状态改变之后,调用到BluetoothSettings的
onBluetoothStateChanged(因为BluetoothSettings覆写了DeviceListPreferenceFragmentonBluetoothStateChanged)

因此,进入BluetoothSettings的onBluetoothStateChanged方法如下:

@Override
    public void onBluetoothStateChanged(int bluetoothState) {
        super.onBluetoothStateChanged(bluetoothState);
        updateContent(bluetoothState);
    }

啊,这再简单不过了。看到updateContent,就知道是去更新BluetoothSettings的界面去了。如下

前面已经大致浏览了蓝牙关闭的状态,现在进入蓝牙打开的状态

private void updateContent(int bluetoothState) {
        int messageId = 0;

        switch (bluetoothState) {
            case BluetoothAdapter.STATE_ON:
                displayEmptyMessage(false);
                mDevicePreferenceMap.clear();

                if (isUiRestricted()) {
                    messageId = R.string.bluetooth_empty_list_user_restricted;
                    break;
                }

                addDeviceCategory(mPairedDevicesCategory,
                        R.string.bluetooth_preference_paired_devices,
                        BluetoothDeviceFilter.BONDED_DEVICE_FILTER, true);
                mPairedDevicesCategory.addPreference(mPairingPreference);
                updateFooterPreference(mFooterPreference);

                if (mAlwaysDiscoverable != null) {
                    mAlwaysDiscoverable.start();
                }
                return; // not break

                //省略部分
        }

        displayEmptyMessage(true);
        if (messageId != 0) {
            getEmptyTextView().setText(messageId);
        }
    }
  1. displayEmptyMessage根据不同的状态,显示不同的UI部分
  2. addDeviceCategory 显示已经配对的设备
  3. updateFooterPreference显示剩下的UI界面

进入displayEmptyMessage如下:

@VisibleForTesting
    void displayEmptyMessage(boolean display) {
        final Activity activity = getActivity();
        activity.findViewById(android.R.id.list_container).setVisibility(
                display ? View.INVISIBLE : View.VISIBLE);
        activity.findViewById(android.R.id.empty).setVisibility(
                display ? View.VISIBLE : View.GONE);
    }

即显示android.R.id.list_container,隐藏android.R.id.empty。

在前面的PreferenceFragemen的默认界面(R.layout.preference_list_fragment),就有android.R.id.list_container
的声明。

可是这里面出现了另外一个问题,android.R.id.list_container里面的内容可是空的呀,Settings界面是如何将自己要用的UI
添加到里面去的呢?

对于PreferenceFragement来说,onCreatePreferences函数,用于创建preferen,并调用addPreferencesFromResource将
创建的Preference加入PreferenceFragment中。

接下来查看BluetoothSettings的onCreatePreferences。在BluetoothSettings的父类DashboardFragment中有onCreatePreferences
如下:

@Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        super.onCreatePreferences(savedInstanceState, rootKey);
        refreshAllPreferences(getLogTag());
    }

调用refreshAllPreferences函数,如下

private void refreshAllPreferences(final String TAG) {
        //省略部分
        // Add resource based tiles.
        displayResourceTiles();
        //省略部分
        
    }

进入displayResourceTiles,如下:

private void displayResourceTiles() {
        final int resId = getPreferenceScreenResId();
        if (resId <= 0) {
            return;
        }
        addPreferencesFromResource(resId);
        final PreferenceScreen screen = getPreferenceScreen();
        Collection<AbstractPreferenceController> controllers = mPreferenceControllers.values();
        for (AbstractPreferenceController controller : controllers) {
            controller.displayPreference(screen);
        }
    }
  1. 从具体的子类中获取资源ID
  2. 调用addPreferencesFromResource,将整个PreferenceScreen加入PreferenceFragment中
  3. 通知所有的PreferenceController,更改了PreferenceScreen

而BluetoothSettings.java的getPreferenceScreenResId(),返回的是R.xml.bluetooth_settings。

因此接下来调用addPreferencesFromResource(R.xml.bluetooth_settings)将xml中的描述加入PreferenceFragment中。

现在进入addPreferencesFromResource,如下:

public void addPreferencesFromResource(@XmlRes int preferencesResId) {
        //省略部分
        setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext,
                preferencesResId, getPreferenceScreen()));
    }

关键部分为setPreferenceScreen函数,如下

public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
        if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
            onUnbindPreferences();
            mHavePrefs = true;
            if (mInitDone) {
                postBindPreferences();
            }
        }
    }
  1. 将PreferenceScreen放入mPreferenceManager中,进行保存
  2. 把以前的PreferenceScreen去掉,即onUnbindPreferences()函数的执行结果
  3. postBindPreferences()通知UI线程绘制整个PreferenceScreen

关键在postBindPreferences中,如下:

private void postBindPreferences() {
        if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
        mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {

                case MSG_BIND_PREFERENCES:
                    bindPreferences();
                    break;
            }
        }
    };

    private void bindPreferences() {
        final PreferenceScreen preferenceScreen = getPreferenceScreen();
        if (preferenceScreen != null) {
            getListView().setAdapter(onCreateAdapter(preferenceScreen));
            preferenceScreen.onAttached();
        }
        onBindPreferences();
    }

最后会掉到bindPreferences()函数
该函数首先,获取mPreferenceManager中保存的PreferenceScreen,然后调用onCreateAdapter,遍历PreferenceScreen
中的层级,创建对应的Adapter,并将其设置给getListView()的返回值。

我们需要寻找的关键点就在getListView()的返回值上面。看看它是不是android.R.id.list_container或者是其子View

在PreferenceFragement的onCreateView中可看到它的赋值,前面已经有其代码,现在再次摘录关键部分如下:

@Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
                savedInstanceState);
        mList = listView;
        listContainer.addView(mList);
        return view;
    }

首先,调动onCreateRecyclerView,创建一个RecyclerView,并将其添加进listContainer。这里listContainer,即是android.R.id.list_container。mList即getListView()的返回值。

自此,蓝牙界面的所有UI都正式梳理完成。总结如下:

  1. 在BluetoothSettings的创建之初,会调用其父类PreferenceFragment的onCreateView,将一个默认的UI界面添加到Windos中
  2. 默认界面有两个重要的组件一个是R.id.list_container,一个是R.id.empty.他们分别存放蓝牙打开是的界面。和蓝牙关闭时的界面.在蓝牙打开和关闭的情况下,就分别显示和隐藏不同的组件。
  3. R.id.lis_container中的蓝牙打开时的具体的细节,则在PreferenceFragment中的onCreate中调用onCreatePreferences,将相应的PreferenceScreen加入进去。
  4. 而蓝牙界面的整体SwitchBar相关的UI,来自于SettingsActivity中的渲染。
  5. ActionBar的UI,则来自于SettingsDrawerActivity的设置。

上面只是列出了整个UI界面的创建过程,对于其销毁过程,几乎具有对称作用,因此不再做过多讨论。

这里面的内容,其实应该属于Settings部分,权且当做蓝牙的开篇吧。

下一篇就进入真正的蓝牙的扫描,配对,以及连接的分析。