android 藍(lán)牙設(shè)備 ota dfu升級(jí)實(shí)錄

image.png

????最近做了一個(gè)小功能,由于沒有需求,只有一個(gè)一代的app services功能實(shí)現(xiàn)進(jìn)行提示。由于更換了外包廠商,所以在升級(jí)版本上需要自行研發(fā)。然而一直從事底層開發(fā)的我,一臉懵逼,后來(lái)驗(yàn)證了,這根本就是n臉懵逼。
????首先下載dex2jar對(duì)apk進(jìn)行反編譯,然后用jd-gui打開。經(jīng)理說(shuō)可以參考這個(gè)進(jìn)行開發(fā),我當(dāng)時(shí)一看這不是很easy么,源碼都有了,再編譯一下就成。然而實(shí)在是太年輕,當(dāng)時(shí)不明白這些變量的名字為什么這么奇怪,后來(lái)才知道,apk經(jīng)過(guò)混淆,變量名都變了的。首先摸清楚業(yè)務(wù)邏輯就花了大力氣,因?yàn)榛煜^(guò)的app反解出來(lái)的代碼,邏輯不完全一樣,之可以知道大概做了些什么事,至于邏輯,需要重新組織。
????至于需要使用的第三方dfu library也是不甚了解,這里將上面反解出來(lái)的classes-dex2jar.jar作為jar包,放入到android studio工程中的app/libs即可以完成dfu library庫(kù)的導(dǎo)入。這里就可以使用第三方的接口進(jìn)行升級(jí)操作了。首先新建一個(gè)android項(xiàng)目,然后如上導(dǎo)入jar包,然后開始創(chuàng)建reciever。

package com.包名;

import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;

import java.lang.reflect.Method;
import java.io.File;

public class ControllerOTARec extends BroadcastReceiver {
    private static final String TAG = "ControllerRec";
    private static final String ACTION_READY_FOR_OTA = "com.action.readyForOTA";
    private static final String DEVICE_NAME = "DEVICE";
    public static final String RETRY_OTA_UPGRADE = "retry.ota.upgrade";
    private static final String BT_CONNECTED = "android.bluetooth.device.action.ACL_CONNECTED";
    private static final String GET_HAND_VERSION ="com.test.gethandversion";
    private  static final File otaFile = new File(ControllerOTAService.mFile);
    public static boolean mHaveDisable = false;
    public static boolean mHaveStart = false;
    private int mOldVersion;
    private int mNewVersion;
    private Method getStringMethod = null;
    public  static BluetoothDevice mDevice = null;


    @Override
    public void onReceive(final Context context, final Intent intent0)
    {
        String actionStr =  intent0.getAction();

        if (BT_CONNECTED.equals(actionStr))
        {
            mDevice = (BluetoothDevice)intent0.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
        } else if(ACTION_READY_FOR_OTA.equals(actionStr)){
            mNewVersion = getStringMethod("ro.build.controller.version", null);
            int mOldVersion = Integer.parseInt(getStringMethod("sys.hand.appVersion","1"));

            //catch if target version lower than present
            if ((mNewVersion <= mOldVersion) || (mOldVersion == 0)) {
                Log.d(TAG, "controller version is very new,not ota!");
                return;
            }

            //catch if zipfile is not exist
            if (!otaFile.exists()) {
                Log.d(TAG, "ota file not exists");
                return;
            }

            //begin to do the things
            if (mDevice != null) {
                final String deviceName = mDevice.getName();
                final String deviceAddr = mDevice.getAddress();
                int deviceBond = mDevice.getBondState();
                Log.d(TAG, "deviceName:" + deviceName + ";and bond state is  " + deviceBond + "; and device address is " + deviceAddr);

                //通知藍(lán)牙狀態(tài)機(jī)馬上進(jìn)入ota模式,停止
                if(!mHaveDisable) {
                    Log.d(TAG, "tell the bluetooth machine to stop for ota");
                    Intent intent = new Intent("bluetooth_ota_update");
                    intent.putExtra("start_bluetooth_state", false);
                    context.sendBroadcast(intent);

                    //delay 500ms for bt to stop
                    try {
                        Thread.sleep(500);
                    } catch (Exception e){
                        Log.d(TAG, "Sleep error");
                    }
                }


                if (!mHaveStart) {
                    removeBondStatus(mDevice);//這是清楚藍(lán)牙配對(duì),這是遇到的最大的一個(gè)問(wèn)題
                    //儲(chǔ)存設(shè)備地址和設(shè)備名信息
                    Log.d(TAG, "mHaveStart = true");
                    ControllerOTARec.mHaveStart = true;
                    SharedPreferences.Editor editor = context.getSharedPreferences("Controller_OTA", 0).edit();
                    editor.putString("deviceName", deviceName);
                    editor.putString("deviceAddress", deviceAddr);
                    editor.commit();

                    //告訴cs開始進(jìn)入ota模式
                    Log.d(TAG,"will tell the controller service to stop for ota");
                    Intent intent1 = new Intent("com.startsignal");
                    context.sendBroadcast(intent1);

                    //準(zhǔn)備工作完成,啟動(dòng)服務(wù)
                    Log.d(TAG, "start ota:" + mDevice.getAddress());
                    Intent intent2 = new Intent(context, ControllerOTAService.class);
                    intent2.putExtra("deviceName", deviceName);
                    intent2.putExtra("deviceAddress", deviceAddr);
                    context.startService(intent2);
                }
            }
        } else if(RETRY_OTA_UPGRADE.equals(actionStr)){
            Log.d(TAG, "wille retry for hand devices ota");
            Intent intent = new Intent(context, ControllerOTAService.class);
            String add = context.getSharedPreferences("Controller_OTA",0).getString("deviceAddress","default");
            intent.putExtra("deviceName",DEVICE_NAME);
            intent.putExtra("deviceAddress",add);
            context.startService(intent);
        }
    }
    //反射的方式訪問(wèn)系統(tǒng)屬性
    public String getStringMethod(final String key, final String def) {
        try {
            if (getStringMethod == null) {
                getStringMethod = Class.forName("android.os.SystemProperties").getMethod("get", String.class, String.class);
            }
            return ((String) getStringMethod.invoke(null, key, def)).toString();
        } catch (Exception e) {
            return def;
        }
    }
    //反射的方式訪問(wèn)藍(lán)牙設(shè)備的隱藏接口removebond
    public void removeBondStatus(BluetoothDevice btDevices){
        boolean result = false;
        try {
            final Method removeBondStatus = btDevices.getClass().getMethod("removeBond");
            if (removeBondStatus != null) {
                result = (Boolean) removeBondStatus.invoke(btDevices);
                Log.w(TAG, "removeBondStatus result is " + result);

                while (btDevices.getBondState() != BluetoothDevice.BOND_NONE){
                    Thread.sleep(20);
                }
            }

        } catch (final Exception e) {
            Log.w(TAG, "An exception occurred while removing bond information", e);
        }
    }
}

然后創(chuàng)建服務(wù)

package com.包名;

import android.app.ProgressDialog;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import java.io.File;

import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;

public class ControllerOTAService extends Service {
    //定義安裝包路徑
    public static final String ZIP_FILE_PATH = "/sdcard/Controller/";
    //獲取安裝包文件名
    public static final String mFile = getmPathFile();
    //進(jìn)度dialog
    private ProgressDialog mDialog;
    //通過(guò)路徑找文件名
    public  static String getmPathFile(){
        String otapackagepath = null;
        File file = new File(ZIP_FILE_PATH);
        File[] array = file.listFiles();
        if(array[0].isFile()){
            otapackagepath = ZIP_FILE_PATH +array[0].getName();
            Log.d("ControllerOTAService", "file path is :" + otapackagepath);
        } else{
            Log.d("ControllerOTAService", "can not find the zip file");
        }
        return  otapackagepath;
    }

    public ControllerOTAService() {
    }
    //注銷監(jiān)聽,停止服務(wù)
    private void stopService()
    {
        DfuServiceListenerHelper.unregisterProgressListener(this, mDfuProgressListener);//取消監(jiān)聽升級(jí)回調(diào)
        stopSelf();
    }
   //啟動(dòng)升級(jí)流程
    private void beginToUpdate(String devname, String addr)
    {
        Log.d("ControllerOTAService", "begin to update hand device by DFU,create dialog");
        createDialog();
        if (this.mDialog != null)
            this.mDialog.show();

        Log.d("ControllerOTAService", "devices name is " + devname +", address is " + addr + ", zip file path is " + mFile);
        final DfuServiceInitiator dfuservice = new DfuServiceInitiator(addr)
                .setDeviceName(devname)
                .setKeepBond(true);//升級(jí)完成后保持連接
        dfuservice.setZip(null, mFile);
        dfuservice.start(this, DfuService.class);

        DfuServiceListenerHelper.registerProgressListener(this, mDfuProgressListener);//注冊(cè)監(jiān)聽

        Log.d("ControllerOTAService", "dismiss dialog");
        //取消dialog
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(5000);
                    ControllerOTAService.this.mDialog.dismiss();
                } catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();

    }
   //創(chuàng)建dialog
    private void createDialog()
    {
        Log.d("ControllerOTAService", "create dialog");
        this.mDialog = new ProgressDialog(this);
        this.mDialog.setMax(100);
        this.mDialog.setProgress(0);
        this.mDialog.setProgressStyle(1);
        this.mDialog.setTitle("hand ota");
        this.mDialog.setMessage("start update hand device,please use head mode");
        this.mDialog.setIndeterminate(false);
        this.mDialog.setCancelable(true);
        this.mDialog.getWindow().setType(2003);
    }
   //startservice會(huì)調(diào)到這里
    public int onStartCommand(Intent intent, int int1, int int2)
    {
        String devname = intent.getStringExtra("deviceName");
        String addr = intent.getStringExtra("deviceAddress");
        Log.d("ControllerOTAService", "deviceName:" + devname + " deviceAddress:" + addr);
        beginToUpdate(devname, addr);
        return Service.START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
    //新建dfu升級(jí)流程監(jiān)聽(新建后自動(dòng)彈出接口函數(shù))
    private  final DfuProgressListener mDfuProgressListener = new DfuProgressListener() {
        //device connecting
        @Override
        public void onDeviceConnecting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceConnecting");
        }
        //devices begin to connect
        @Override
        public void onDeviceConnected(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceConnected");
        }
        //before ota process start
        @Override
        public void onDfuProcessStarting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuProcessStarting");
        }

        @Override
        public void onDfuProcessStarted(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuProcessStarted");
        }

        @Override
        public void onEnablingDfuMode(String deviceAddress) {
            Log.d("ControllerOTAService", "onEnablingDfuMode");
        }

        @Override
        public void onProgressChanged(String deviceAddress, int percent, float speed, float avgSpeed, int currentPart, int partsTotal) {
            Log.d("ControllerOTAService", "onProgressChanged");
        }

        @Override
        public void onFirmwareValidating(String deviceAddress) {
            Log.d("ControllerOTAService", "onFirmwareValidating");
        }

        @Override
        public void onDeviceDisconnecting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceDisconnecting");
        }

        @Override
        public void onDeviceDisconnected(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceDisconnected");
        }
        //完成升級(jí)會(huì)調(diào)入到這里
        @Override
        public void onDfuCompleted(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuCompleted");
            //發(fā)出停止廣播
            Intent intent = new Intent("com.handota.stop");
            ControllerOTAService.this.sendBroadcast(intent);
            //恢復(fù)狀態(tài)機(jī)
            Intent intent1 = new Intent("bluetooth_ota_update");
            intent1.putExtra("start_bluetooth_state", true);
            ControllerOTAService.this.sendBroadcast(intent1);
            //是否啟動(dòng)ota狀態(tài)改為false
            ControllerOTARec.mHaveStart = false;
            
            //tell app the result
            Intent intent2 = new Intent("hand_device_ota_process");
            intent2.putExtra("handOTAProcess",1);
            ControllerOTAService.this.sendBroadcast(intent2);

            //DFU completed will delete the zip file
            Log.d("ControllerOTAService", "begin to delete the update zipfile");
            clearFile(ZIP_FILE_PATH);

            Log.d("ControllerOTAService", "<<<onDfuCompleted");
            ControllerOTAService.this.stopService();
        }

        @Override
        public void onDfuAborted(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuAborted");

            Intent intent = new Intent("bluetooth_ota_update");
            intent.putExtra("start_bluetooth_state", true);
            ControllerOTAService.this.sendBroadcast(intent);

            ControllerOTARec.mHaveStart = false;

            //tell app the result
            Intent intent2 = new Intent("hand_device_ota_process");
            intent2.putExtra("handOTAProcess",2);
            ControllerOTAService.this.sendBroadcast(intent2);

            //DFU completed will delete the zip file
            Log.d("ControllerOTAService", "begin to delete the update zipfile");
            clearFile(ZIP_FILE_PATH);

            ControllerOTAService.this.stopService();
        }

        @Override
        public void onError(String deviceAddress, int error, int errorType, String message) {
            Log.d("", "ERROR"+error+"message"+message);

            Intent intent = new Intent("bluetooth_ota_update");
            intent.putExtra("start_bluetooth_state", true);
            ControllerOTAService.this.sendBroadcast(intent);

            //tell app the result
            Intent intent2 = new Intent("hand_device_ota_process");
            intent2.putExtra("handOTAProcess",3);
            ControllerOTAService.this.sendBroadcast(intent2);

            ControllerOTARec.mHaveStart = false;

            //DFU completed will delete the zip file
            Log.d("ControllerOTAService", "begin to delete the update zipfile");
            clearFile(ZIP_FILE_PATH);

            ControllerOTAService.this.stopService();
        }
    };
    //刪除文件函數(shù)
    public static void clearFile(String filePath){
        File targetFile = new File(filePath);
        File[] fileList = targetFile.listFiles();
        Log.d("ControllerOTAService", "clearFile");
        if(fileList[0].exists()){
            fileList[0].delete();
            Log.d("ControllerOTAService", "clearFile success");
        }else {
            Log.d("ControllerOTAService", "no file to delete");
        }
    }
}

????這里還要?jiǎng)?chuàng)建dfuservices類

package com.包名;

import android.app.Activity;
import no.nordicsemi.android.dfu.DfuBaseService;

public class DfuService extends DfuBaseService
{
    protected Class<? extends Activity> getNotificationTarget()
    {
        return NotificationActivity.class;
    }

    protected boolean isDebug()
    {
        return true;
    }
}

????創(chuàng)建NotificationActivity通知類

package com.包名;

import android.os.Bundle;
import android.app.Activity;

public class NotificationActivity extends Activity {

    @Override
    protected void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        finish();
    }
}

????配置xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com包名">

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".ControllerOTAService"
            android:enabled="true"
            android:exported="true" />
        <service
            android:name=".DfuService"
            android:enabled="true"
            android:exported="true" />

        <receiver android:name=".ControllerOTARec">
            <intent-filter>
                <action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
                <action android:name="retry.ota.upgrade" />
                <action android:name="com.action.readyForOTA" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

????配合藍(lán)牙狀態(tài)機(jī)升級(jí)流程大致如下


image.png

????遇到的問(wèn)題:
????1.不同版本的官方nordic軟件,升級(jí)方式也不一樣,1.61.3版本連接狀態(tài)下可以直接進(jìn)行升級(jí);2.6.0版本必須設(shè)備通過(guò)硬件進(jìn)入ota模式才能正常升級(jí),同時(shí),高版本的dfu代碼在18年7月已經(jīng)對(duì)O代碼做了適應(yīng),同時(shí)到O有些接口發(fā)生了變化,得確認(rèn)是否在自己項(xiàng)目里面支持,不對(duì)應(yīng)的話,需要下載對(duì)應(yīng)的dfu版本的三方庫(kù);
????2.讀取設(shè)備版本這里遇到過(guò)一些問(wèn)題,由于代碼設(shè)計(jì)問(wèn)題,設(shè)備當(dāng)前版本在目前項(xiàng)目里面經(jīng)常被重置為1,這里采取臨時(shí)的系統(tǒng)屬性來(lái)儲(chǔ)存正確讀取的當(dāng)前版本;
????3.由于一開始使用的是反編譯的apk做jar包,有些庫(kù)函數(shù)并不能被反解出來(lái),導(dǎo)致內(nèi)部邏輯無(wú)法獲知,后來(lái)在github上下載了源碼,就可以看到了;
????4.遇到一個(gè)讀取dfu版本失敗的問(wèn)題,追Src read.p_value ptr is NULL發(fā)現(xiàn)深陷藍(lán)牙協(xié)議棧中的調(diào)用無(wú)法自拔,找不到根本原因,追Reading DFU version number發(fā)現(xiàn)是dfuservices要發(fā)起一此讀取dfu版本信息的請(qǐng)求,然后gattserver收到這個(gè)請(qǐng)教,通過(guò)jni,向藍(lán)牙協(xié)議棧發(fā)送了這個(gè)請(qǐng)求,然后返回的結(jié)果不對(duì),status為1,正確的情況status為0,一開始藍(lán)牙這一塊的東西什么都不懂的時(shí)候有點(diǎn)摸不清頭腦。為什么這里要去讀這個(gè),而且這個(gè)讀取操作在升級(jí)過(guò)程中有兩到三次。整個(gè)升級(jí)流程也是一頭霧水。后來(lái)慢慢的梳理log,發(fā)現(xiàn)在gatt server向手柄發(fā)送了進(jìn)入ota模式的命令后,然后再去讀取手柄相關(guān)屬性就會(huì)失敗。
????這里涉及到一個(gè)隱藏任務(wù),設(shè)備在ota模式和非ota模式,屬性值會(huì)發(fā)生變化,在手柄進(jìn)入ota模式后,主機(jī)設(shè)備的藍(lán)牙服務(wù)還會(huì)進(jìn)行一次掃描,然后才會(huì)開始發(fā)送安裝包進(jìn)行升級(jí)的操作。然而新產(chǎn)品上為什么協(xié)議棧沒有進(jìn)行掃描,這一點(diǎn)需要一定藍(lán)牙知識(shí)進(jìn)行調(diào)查可能會(huì)更快解決。這里的臨時(shí)做法,就是先進(jìn)行設(shè)備配對(duì)信息清除,清楚完之后,設(shè)備就會(huì)進(jìn)行掃描(可能跟產(chǎn)品里面的狀態(tài)機(jī)有關(guān)系,在有配對(duì)信息的情況下,就不會(huì)進(jìn)行掃描。)。即是這里的removeBondStatus(mDevice);這一塊困住了很長(zhǎng)時(shí)間,在宇神的一次操作下成功繞出去了,就得到了這個(gè)workaround。

DfuImpl : Reading DFU version number...
WCNSS_FILTER: ibs_recv_ibs_cmd: Received IBS_WAKE_IND: 0xFD
WCNSS_FILTER: ibs_recv_ibs_cmd: Writing IBS_WAKE_ACK
WCNSS_FILTER: do_write: IBS write: fc
bt_btif : btapp_gattc_req_data :Src read.p_value ptr is NULL for event  0x3
BluetoothGatt: onCharacteristicRead() - Device=D0:F0:63:61:D4:B1 handle=28 Status=1
DfuImpl : Characteristic read error: 1
DfuBaseService: Unable to read version number (error 1)
DfuBaseService: Disconnecting from the device...
BluetoothGatt: cancelOpen() - device: D0:F0:63:61:D4:B1
BtGatt.GattService: clientDisconnect() - address=D0:F0:63:61:D4:B1, connId=8
BtGatt.GattService: onDisconnected() - clientIf=8, connId=8, address=D0:F0:63:61:D4:B1
BluetoothGatt: onClientConnectionState() - status=0 clientIf=8 device=D0:F0:63:61:D4:B1
DfuBaseService: Disconnected from GATT server
DfuBaseService: Cleaning up...
BluetoothGatt: close()
BluetoothGatt: unregisterApp() - mClientIf=8

????5.apk實(shí)現(xiàn)后,怎樣將android studio集成到android source項(xiàng)目當(dāng)中去也是因?yàn)椴惶煜み@塊有太多的問(wèn)題,首先是sdk版本過(guò)高,將apk放入android項(xiàng)目失敗的問(wèn)題。這里去android studio中手動(dòng)從O降級(jí)到N,第三方庫(kù)的降級(jí)方法就是一個(gè)個(gè)去解決編譯錯(cuò)誤,找到對(duì)應(yīng)的地版本對(duì)象進(jìn)行替換。這里的25都是由27轉(zhuǎn)換過(guò)來(lái)的。

apply plugin: 'com.android.application'

android {
    signingConfigs {
        config {
            keyAlias 'controllerota'
            keyPassword '123456'
            storeFile file('/home/edward/bin/android-studio/keystore/key.jks')
            storePassword '123456'
        }
    }
    compileSdkVersion 25
    defaultConfig {
        applicationId "com.qiyi.controllerota1"
        minSdkVersion 23
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:25.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    implementation files('libs/DFUlibrary.jar')
}

????6.關(guān)于系統(tǒng)ro屬性的修改方法,在前一篇中記錄了,之修改buildinfo.sh似乎是不奏效的。
????最后一點(diǎn)忠告:就是盡量做需求清晰,設(shè)計(jì)明確的工作,降低跟同事工作的耦合度---------------雖然空白做法也挺鍛煉人。
????完全知識(shí)分享,謝謝支持

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容