zoukankan      html  css  js  c++  java
  • Android4.42-Settings源代码分析之蓝牙模块Bluetooth(上)

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


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


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

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


    <!-- Bluetooth -->
        <header
            .......
            android:fragment="com.android.settings.bluetooth.BluetoothSettings"
            ......./>
    
           2>。所涉及到的清单配置文件里的属性具体解释,清单文件里介绍了蓝牙界面启动相关的一些设置。诸如有快捷方式入口,以及是否隐藏进程等等,在这里大致对一些不常见的属性进行说明。方便查阅

    <activity android:name="......"
                    android:uiOptions="splitActionBarWhenNarrow"
                    android:configChanges="orientation|keyboardHidden|screenSize"
                    android:label="@string/bluetooth_settings_title"
                    android:taskAffinity=""
                    android:excludeFromRecents="true">
                <intent-filter>
                   ......
                </intent-filter>
                <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                    android:value="com.android.settings.bluetooth.BluetoothSettings" />
                <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
                    android:resource="@id/bluetooth_settings" />
            </activity>
    
            <!-- Keep compatibility with old shortcuts. -->
            <activity-alias android:name=".bluetooth.BluetoothSettings"
                    
                    android:label="@string/bluetooth_settings_title"
                    android:targetActivity="Settings$BluetoothSettingsActivity"
                    android:exported="true"
                    android:clearTaskOnLaunch="true">
                <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
                    android:value="com.android.settings.bluetooth.BluetoothSettings" />
                <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
                    android:resource="@id/bluetooth_settings" />
            </activity-alias>

    能够看到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-alias../>节点。该节点属于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,代表显示整个屏幕,内部可嵌套不同类型的标签,在这里内部未有不论什么标签,是在代码中动态加入的不同种类的布局。


    <PreferenceScreen
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:title="@string/bluetooth_settings" >
    </PreferenceScreen>
    
       

        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有一个设置



    当中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)方法代码非常easy,不再赘述

    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);
                        }
                    }
                      <pre name="code" class="java">                // 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(下)








  • 相关阅读:
    货币
    沙漏
    秋季学习总结
    三个老师
    介绍自己
    redis 的部分配置
    第二次博客作业
    第一次阅读作业
    shell_通配符
    shell_/dev/null,>和&
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/7184034.html
Copyright © 2011-2022 走看看