溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

MonkeyDevcie API 實踐全記錄

發(fā)布時間:2020-05-19 01:08:36 來源:網(wǎng)絡(luò) 閱讀:442 作者:zhukev 欄目:移動開發(fā)

1.    背景

使用SDK自帶的NotePad應(yīng)用作為實踐目標應(yīng)用,目的是對MonkeyDevice擁有的成員方法做一個初步的了解。

以下是官方列出的方法的Overview。

Return Type

Methods

Comment

void

broadcastIntent (string uri, string action, string data, string mimetype, 

iterable categories dictionary extras, component component, iterable flags)

Broadcasts an Intent to this device, as if the Intent were coming from an application.

 

void

drag (tuple start, tuple end, float duration, integer steps)

Simulates a drag gesture (touch, hold, and move) on this device's screen.

 

object

getProperty (string key)

Given the name of a system environment variable, returns its value for this device.

The available variable names are listed in the detailed description of this method.

 

object

getSystemProperty (string key)

. The API equivalent of adb shell getprop <key>. This is provided for

use by platform developers.

 

void

installPackage (string path)

Installs the Android application or test package contained in packageFile onto this device.

If the application or test package is already installed, it is replaced.

Obsolete,返回值是Boolean

dictionary

instrument (string className, dictionary args)

Runs the specified component under Android instrumentation, and returns the results

 in a dictionary whose exact format is dictated by the component being run.

 The component must already be present on this device.

 

void

press (string name, dictionary type)

Sends the key event specified by type to the key specified by keycode.

 

void

reboot (string into)

Reboots this device into the bootloader specified by bootloadType.

 

void

removePackage (string package)

Deletes the specified package from this device, including its data and cache.

 Obsolete,返回值是Boolean

object

shell (string cmd)

Executes an adb shell command and returns the result, if any.

 

void

startActivity (string uri, string action, string data, string mimetype, iterable categories 

dictionary extras, component component, flags)

Starts an Activity on this device by sending an Intent constructed from the supplied arguments.

 

MonkeyImage

takeSnapshot()

Captures the entire screen buffer of this device, yielding a MonkeyImage object containing

a screen capture of the current display.

 

void

touch (integer x, integer y, integer type)

Sends a touch event specified by type to the screen location specified by x and y.

 

void

type (string message)

Sends the characters contained in message to this device, as if they had been typed on

the device's keyboard. This is equivalent to callingpress() for each keycode in message 

using the key event type DOWN_AND_UP.

 

void

wake ()

Wakes the screen of this device.

 


其實官方這個表是沒有及時更新的,我現(xiàn)在手頭上用到的MonkeyRunner是當前最新的,里面就擁有好幾個官網(wǎng)沒有列出來的API,我懷疑是不是自從UIAutomator在03年出來后,google就不打算再繼續(xù)維護MonkeyRunner了?如果有朋友知道事實的話,還麻煩告知。

以下是我整理出來的源碼多出來的可用公共API列表

Return Type

Methods

Comment

HierarchyViewer

getHierarchyViewer(PyObject args[], String kws[])

獲取一個HierarchyViewer對象

 請查看《MonkenRunner通過HierarchyViewer定位控件的方法和建議》

PyList

getPropertyList(PyObject args[], String kws[])

 取得所有的property屬性鍵值

PyList

getViewIdList(PyObject args[], String kws[])

 Failed

MonkeyView

getViewById(PyObject args[], String kws[])

 Failed

MonkeyView

getViewByAccessibilityIds(PyObject args[], String kws[])

 Failed

MonkeyView

getRootView(PyObject args[], String kws[])

 Failed

PyList

getViewsByText(PyObject args[], String kws[])

 Failed

但可惜的是在本人嘗試以上多出來的API的時候,發(fā)現(xiàn)除了最上面兩個可用之外,其他的都不可用并拋出錯誤。且網(wǎng)上資料少的可憐,別人碰到同樣的問題也找不到解決辦法。所以本人懷疑這些“隱藏”API是不是并沒有完善,或者說google不準備完善,所以才沒有列出到官網(wǎng)上面去。本人用的SDK tools和Platform tools已經(jīng)是當前最新的23.0.2和20.

一個臺灣網(wǎng)友碰到的問題描述:http://imsardine.simplbug.com/note/monkeyrunner/api/hierarchy-viewer.html


2.    Void broadcastIntent 

(string uri, string action,string data, string mimetype, iterable categories dictionary extras, componentcomponent, iterable flags)

2.1 分析

本人理解的此方法的本意是想廣播一個Intent給我們的AndroidDevice,目標應(yīng)用接收到該Intent做相應(yīng)的處理,比如打開一個Activity等。但在我的多次嘗試下并沒有成功!

targetDevice.broadcastIntent(action='android.intent.action.INSERT',                            mimetype='vnd.android.cursor.dir/contact',                            extras = {'name':'user1501488', 'phone':'123-15489'} 

如果使用同樣的參數(shù),使用下面的startActivity是沒有問題的。

targetDevice.startActivity(action='android.intent.action.INSERT',                            mimetype='vnd.android.cursor.dir/contact',                            extras = {'name':'user1501488', 'phone':'123-15489'}) 

google了半天網(wǎng)上根本找不到這個方法的使用例子,倒是stackOverFlow上有人建議用Shell來達到同樣的效果。

targetDevice.shell("am start -a android.intent.action.INSERT -t vnd.android.cursor.dir/contact -e name 'Donald Duck' -e phone 555-1234").

所以可見這個方法并沒有多少人在用,原因應(yīng)該是它完全可以用上面介紹的兩個方法替代。


3. void startActivity 

(string uri, string action, string data, string mimetype, iterable categories dictionary extras, component component, flags)

3.1 示例

使用Action和mimetype來啟動一個Activity:
targetDevice.startActivity(action='android.intent.action.VIEW',                           mimetype='vnd.android.cursor.dir/vnd.google.note')

使用component來啟動一個Activity:
targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList")

使用action,mimetype和指定extras參數(shù)來啟動一個Activity:
targetDevice.startActivity(action='android.intent.action.INSERT',                            mimetype='vnd.android.cursor.dir/contact',                            extras = {'name':'user1501488', 'phone':'123-15489'})

3.2 分析

這個方法存在和以上的broadcastInt方法一樣擁有同樣的一大串參數(shù),實例中只給出了本人已經(jīng)跑通的例子,其他參數(shù)怎么用還有待深入研究,不過我相信對于我自己來說暫時這樣子已經(jīng)足夠了。
另外需要注意的是參數(shù)中如果我們的填寫不是按順序從第一個參數(shù)開始的話,記得要在每個參數(shù)值前面指定你要傳入的是哪個參數(shù),不然默認就會從第一個參數(shù)開始算,這是python的基本語法了,這里就不展開了。

4.  void drag (tuple start, tuple end, floatduration, integer steps)

這個方法的目的是按住一個控件然后把她拖動到其他位置

4.1 實例

viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y  targetDevice.drag((startX,startY),(startX,startY),1)  targetDevice.press('KEYCODE_BACK', MonkeyDevice.DOWN_AND_UP) 

4.2 分析和建議

以上示例是通過drag的方法來模擬LongPress,只要把參數(shù)中的起始坐標和目標坐標都設(shè)置成同樣一個值,然后時常設(shè)置成一個有效的值就好了。


5 Object getProperty (string key)

 通過環(huán)境變量的key來獲得其對應(yīng)的值。以下是官方提供的可用化境變量列表

Property Group

Property

Description

Notes

build

board

Code name for the device's system board

See Build

brand

The carrier or provider for which the OS is customized.

 

device

The device design name.

 

fingerprint

A unique identifier for the currently-running build.

 

host

 

 

ID

A changelist number or label.

 

model

The end-user-visible name for the device.

 

product

The overall product name.

 

tags

Comma-separated tags that describe the build, such as "unsigned" and "debug".

 

type

The build type, such as "user" or "eng".

 

user

 

 

CPU_ABI

The name of the native code instruction set, in the form CPU type plus ABI convention.

 

manufacturer

The product/hardware manufacturer.

 

version.incremental

The internal code used by the source control system to represent this version of the software.

 

version.release

The user-visible name of this version of the software.

 

version.sdk

The user-visible SDK version associated with this version of the OS.

 

version.codename

The current development codename, or "REL" if this version of the software has been released.

 

display

width

The device's display width in pixels.

SeeDisplayMetricsfor details.

height

The device's display height in pixels.

 

density

The logical density of the display. This is a factor that scales DIP (Density-Independent Pixel) units to the device's resolution. DIP is adjusted so that 1 DIP is equivalent to one pixel on a 160 pixel-per-inch display. For example, on a 160-dpi screen, density = 1.0, while on a 120-dpi screen, density = .75.

The value does not exactly follow the real screen size, but is adjusted to conform to large changes in the display DPI. See density for more details.

 

am.current

package

The Android package name of the currently running package.

The am.currentkeys return information about the currently-running Activity.

action

The current activity's action. This has the same format as the name attribute of the action element in a package manifest.

 

comp.class

The class name of the component that started the current Activity. See comp.package for more details.

 

comp.package

The package name of the component that started the current Activity. A component is specified by a package name and the name of class that the package contains.

 

data

The data (if any) contained in the Intent that started the current Activity.

 

categories

The categories specified by the Intent that started the current Activity.

 

clock

realtime

The number of milliseconds since the device rebooted, including deep-sleep time.

SeeSystemClock for more information.


5.1示例

displayWidth =targetDevice.getProperty ('display.width') printdisplayWidth.encode('utf-8')   displayHight =targetDevice.getProperty('display.width') printdisplayHight.encode('utf-8')  

5.2  分析和建議

以上示例的目的是獲得目標設(shè)備的長和高,當我們使用坐標點來操作控件的時候,調(diào)試的時候在一臺機器上通過了,但是如果換了另外一個屏幕大小不一樣的機器的話就會失敗,因為控件的坐標點位置可能就變了。這個時候我們就需要用到示例中的連個屬性來動態(tài)計算控件在不同屏幕大小的設(shè)備上面的坐標點了。

這里需要注意參數(shù)應(yīng)該填寫的格式是以上列表中前兩列的組合PropertyGroup.Property


6. Object getSystemProperty (string key)

6.1 示例

displayWidth = targetDevice.getSystemProperty ('service.adb.tcp.port') print displayWidth.encode('utf-8') 

6.2 分析和建議

根據(jù)官網(wǎng)的描述,這個函數(shù)和getProperty函數(shù)應(yīng)該有同樣的功能(Synonym for getProperty().),使用的屬性表也如上面的屬性列表一樣。但是按照我的實踐并非如此,不過它確實如官方描述的等同于命令“adb shell getprop <key>.”倒是真的。

以上的例子是獲取adb這個服務(wù)所打開的TCP端口,等同于如下的shell命令:“adb shell getprop service.adb.tcp.port

如果我嘗試使用下面的方法去獲得設(shè)備的長度,返回的結(jié)果其實會是None

displayWidth = targetDevice.getSystemProperty ('display.width') print displayWidth.encode('utf-8') 

7 Boolean installPackage (string path)

7.1示例

if True == targetDevice.installPackage('D:\\Projects\\Workspace\\PythonMonkeyRunnerDemo\\apps\\MPortal.apk'):    print "Installationfinished successfully" else:     print "Failedto install the apk"

7.2 分析和建議

這里有兩點需要注意的:

  • 官方網(wǎng)站描述的這個API是沒有返回值的(見背景中的表),而最新版本的API里面是Boolean值。
  • 參數(shù)輸入的應(yīng)該是PC端這邊的Local路徑而非目標系統(tǒng)的Local路徑。

注意這里碰到一個路徑問題,如果我的路徑寫成:

'D:\Projects\Workspace\PythonMonkeyRunnerDemo\apps\MPortal.apk'

那么會出現(xiàn)下圖這樣的錯誤:

MonkeyDevcie API 實踐全記錄

如果在“apps”前面加多個轉(zhuǎn)義符就沒有問題:

'D:\Projects\Workspace\PythonMonkeyRunnerDemo\\apps\MPortal.apk'

所以這里MonkeyRunner處理路徑字串應(yīng)該是存在Bug的,但是時間問題就先不去研究,建議所有路徑的話“\“都加上轉(zhuǎn)義符就好了

'D:\\Projects\\Workspace\\PythonMonkeyRunnerDemo\\apps\MPortal.apk'


8 boolean removePackage(string package)

8.1 示例

if True == targetDevice.removePackage('com.majcit.portal'):    print "Succeed toremove the package" else:     print "Failedto remove teh package"

8.2 注意事項

·        如上面的installPackageAPI一樣,官網(wǎng)描述的該API是沒有返回值的,而最新的是返回boolean


9. dictionary instrument (string className, dictionary args)

9.1 示例

只指定第一個參數(shù)(將會跑所有指定Component下的所有case+ NotePad項目本身的使用了Instrumentation的用例,具體意思請看下一節(jié)的分析)

dict = targetDevice.instrument('com.example.android.notepad.tryout/android.test.InstrumentationTestRunner') print dict 

指定只跑其中的一個Case:

dict = dict = targetDevice.instrument('com.example.android.notepad.tryout/android.test.InstrumentationTestRunner',                                       {'class':'com.example.android.notepad.tryout.TCCreateNote'}) print dict 

9.2 分析

這里首先需要看下官方的描述

Runs the specifiedcomponent under Android instrumentation, and returns the results in adictionary whose exact format is dictated by the component being run. Thecomponent must already be present on this device

這里重點是第一句“Runs the specified component under Android instrumentation“,剩下的是說返回值根據(jù)模塊不一樣而有所差別。那么第一句翻譯過來就是”運行使用了Anroid Instrumentation的指定模塊“。那么究竟什么是使用了Android Instrumentation的指定模塊呢?其實如果你有用過Robotium的話應(yīng)該知道,腳本的每個case都是從Instrumentation相關(guān)的類中繼承下來的。

以下是本人之前寫的一個Robotium的測試case(具體請查看本人之前的一篇《Robotium創(chuàng)建一個Note的實例》,可以看到它是繼承于“ActivityInstrumentationTestCase2”的,其實它就滿足了剛才的描述“使用了Anroid Instrumentation的指定模塊“

package com.example.android.notepad.tryout;  import com.robotium.solo.Solo;  import android.test.ActivityInstrumentationTestCase2; import android.app.Activity;  @SuppressWarnings("rawtypes") public class TCCreateNote extends ActivityInstrumentationTestCase2{  	private static Solo solo = null; 	public Activity activity; 	 	private static final int NUMBER_TOTAL_CASES = 2; 	private static int run = 0; 	 	private static Class<?> launchActivityClass;  	//對應(yīng)re-sign.jar生成出來的信息框里的兩個值 	private static String mainActiviy = "com.example.android.notepad.NotesList"; 	private static String packageName = "com.example.android.notepad";  	static {  		try {  			launchActivityClass = Class.forName(mainActiviy);  		} catch (ClassNotFoundException e) {  			throw new RuntimeException(e);  		}  	} 	 	 	@SuppressWarnings("unchecked") 	public TCCreateNote() { 		super(packageName, launchActivityClass); 	}  	 	@Override 	public void setUp() throws Exception { 		//setUp() is run before a test case is started.  		//This is where the solo object is created. 		super.setUp();  		//The variable solo has to be static, since every time after a case's finished, this class TCCreateNote would be re-instantiated 		// which would lead to soto to re-instantiated to be null if it's not set as static 		if(solo == null) { 			TCCreateNote.solo = new Solo(getInstrumentation(), getActivity()); 		} 	} 	 	@Override 	public void tearDown() throws Exception { 		//Check whether it's the last case executed. 		run += countTestCases(); 		if(run >= NUMBER_TOTAL_CASES) { 			solo.finishOpenedActivities(); 		} 	}  	public void testAddNoteCNTitle() throws Exception { 		 		solo.clickOnMenuItem("Add note"); 		solo.enterText(0, "中文標簽筆記"); 		solo.clickOnMenuItem("Save"); 		solo.clickInList(0); 		solo.clearEditText(0); 		solo.enterText(0, "Text 1"); 		solo.clickOnMenuItem("Save"); 		solo.assertCurrentActivity("Expected NotesList Activity", "NotesList"); 		 		solo.clickLongOnText("中文標簽筆記"); 		solo.clickOnText("Delete"); 	} 	 	 	public void testAddNoteEngTitle() throws Exception { 		solo.clickOnMenuItem("Add note"); 		solo.enterText(0, "English Title Note"); 		solo.clickOnMenuItem("Save"); 		solo.clickInList(0); 		solo.clearEditText(0); 		solo.enterText(0, "Text 1"); 		solo.clickOnMenuItem("Save"); 		solo.assertCurrentActivity("Expected NotesList Activity", "NotesList"); 		 		solo.clickLongOnText("English Title Note"); 		solo.clickOnText("Delete"); 	} } 

那么我們先在目標機器上執(zhí)行命令“pm list instrumentation”看是否能列出這個“Component”:

MonkeyDevcie API 實踐全記錄

這其中哪一個是我們想要的呢?這要看我們的Robotium測試項目中的AndroidManifest.xml的定義了,根據(jù)下圖的packageName再對照上圖的輸出我們就定位到我們想要的Component了:

MonkeyDevcie API 實踐全記錄

如果我們調(diào)用這個方法的時候只是填寫了第一個參數(shù)的話,上圖處在”com.example.andriod.notepad.tryout”這個Component下的所有三個方法都會執(zhí)行。但在我的測試中,發(fā)現(xiàn)除了跑這個Case之外,它還跑多了一些步驟,就是額外的多創(chuàng)建了一個Note1。我查了半天,發(fā)現(xiàn)原來我之前除了編寫了基于ActivityInstrumentationTestCase2Robotium的這個項目之外,還在NoetePad這個項目上直接用InstrumentationTestCase寫了一個基于Instrumentation的測試用例,做得事情就是增加一個Note1,具體請看本人另外一篇blog《SDK Instrumentation創(chuàng)建一個Note的實例

MonkeyDevcie API 實踐全記錄


其實調(diào)用這個方法相當于在shell腳本上執(zhí)行如下的命令:

am instrument -w -r -e class com.example.android.notepad.tryout.TCCreateNote com.example.android.notepad.tryout/android.test.InstrumentationTestRunner

 

最后提一下的是,我們的腳本在執(zhí)行示例的時候大概5秒左右就會失敗,但事實上在設(shè)備端所有的指定component的測試用例已經(jīng)在執(zhí)行的了。要解決這個問題需要重新編譯源碼,這里就免了,具體請查看:http://stackoverflow.com/questions/4264057/android-cts-is-showing-shellcommandunresponsiveexception-on-emulator

這里嘗試做一個簡單的總結(jié)

  • 調(diào)用此方法之前先要找到第一個參數(shù)怎么寫:執(zhí)行命令“pm list instrumentation
  •  調(diào)用次方法如果不指定第一個參數(shù)會執(zhí)行指定Component下面的所有測試腳本和目標應(yīng)用包中所有用到Instrumentation的測試用例
  • 調(diào)用此方法最終事實上是在目標機器上根據(jù)指定的參數(shù)執(zhí)行“am instrument xxxx”

10 void press (string name, integer type)

10.1 示例

targetDevice.press('KEYCODE_BACK',MonkeyDevice.DOWN_AND_UP)

10.2 分析

這個方法的目的是發(fā)送一個指定的Key事件到Android設(shè)備以觸發(fā)相應(yīng)的動作,比如示例中發(fā)送“KEYCODE_BACK”這個key到設(shè)備端去觸發(fā)”返回“鍵的按下和升起(也就是點擊)的動作。

具體Key的Code請查看:http://developer.android.com/reference/android/view/KeyEvent.html

第二個參數(shù)是指導該Key應(yīng)該如何Behave,比如先按下去,休眠一秒再彈起來(其實就相當于一個長按的動作)。具體支持behavior在本MonkeyDevice這個類的成員變量里有定義好:

Type

Constants

Notes

Comment

int官網(wǎng)是string,下同

DOWN

Use this with the type argument of press() or touch() to send a DOWN event.

 

int

UP

Use this with the type argument of press() or touch() to send an UP event.

 

int

DOWN_AND_UP

Use this with the type argument of press() or touch() to send a DOWN event immediately followed by an UP event.

 

int

MOVE

 TBD

官網(wǎng)沒有列出


11. void touch (integer x,integer y, integer type)

11.1 示例

viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y  targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP) 

11.2 分析

這個和上面的press方法在結(jié)果上有點類似,但是本方法是接受坐標進行點擊的,而上面方法接受的是keycode。

第二個參數(shù)同上。


12. void type (string message)

viewer =targetDevice.getHierarchyViewer()

note = viewer.findViewById('id/text1')

point = viewer.getAbsoluteCenterOfView(note)

startX = point.x

startY = point.y

targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP)

MonkeyRunner.sleep(3)

targetDevice.type('NewContent')

12.1 示例

viewer = targetDevice.getHierarchyViewer() note = viewer.findViewById('id/text1') point = viewer.getAbsoluteCenterOfView(note) startX = point.x startY = point.y targetDevice.touch(startX,startY,MonkeyDevice.DOWN_AND_UP) MonkeyRunner.sleep(3) targetDevice.type('NewContent') 

12.2 分析

指定一串字符串進行模擬鍵盤輸入,等同于調(diào)用press方法按照一個一個的keycode用MonkeyDevice的DOWN_AND_UP進行輸入。

嘗試時發(fā)現(xiàn)中文不支持,鑒于項目不需要用到這種手機鍵盤,所以節(jié)省時間暫不研究。


13. Wake()

13.1示例

targetDevice.wake()

13.2 分析

注意這里只是喚醒屏保,如果手機已經(jīng)鎖屏的話你是不能喚醒的。所以嘗試這個API的時候要注意把鎖屏功能先關(guān)閉掉。


14. MonkeyImange takeSnapshot()

14.1 示例

#Connect to the target targetDevice targetDevice = MonkeyRunner.waitForConnection()  easy_device = EasyMonkeyDevice(targetDevice)  #touch a button by id would need this targetDevice.startActivity(component="com.example.android.notepad/com.example.android.notepad.NotesList")  #invoke the menu options MonkeyRunner.sleep(6) #targetDevice.press('KEYCODE_MENU', MonkeyDevice.DOWN_AND_UP);  '''      public ViewNode findViewById(String id)       * @param id id for the view.      * @return view with the specified ID, or {@code null} if no view found. ''' #MonkeyRunner.alert("Continue?", "help", "Ok?")  pic = targetDevice.takeSnapshot() pic = pic.getSubImage((0,38,480,762))  newPic = targetDevice.takeSnapshot() newPic = newPic.getSubImage((0,38,480,762))  print (newPic.sameAs(pic,1.0)) newPic.writeToFile('./shot1.png','png')


14 object shell(string cmd)

14.1 示例

res = targetDevice.shell('ls /data/local/tmp|grep note') print res

14.2 分析

這個命令等同于你直接在命令行上“adb shell $command


15 void reboot(string into)

其他參數(shù)沒有用到所以也就沒有嘗試,僅僅嘗試了不帶參數(shù)的情況,結(jié)果就是直接reboot目標設(shè)備了

targetDevice.reboot()
這里有點需要提下的是,如果你是在MonkeyRunner命令行下執(zhí)行這條命令的話,就算目標機器重啟,整個MonkeyRunner的環(huán)境還是依然有效的。也就是說你如果繼續(xù)打進一條"targetDevice.reboot()",你的設(shè)備就會再重啟一次。


<div id="ftw3e"><progress id="ftw3e"><menuitem id="ftw3e"></menuitem></progress></div>
  •  

    作者

    自主博客

    微信

    CSDN

    天地會珠海分舵

    http://techgogogo.com


    服務(wù)號:TechGoGoGo

    掃描碼:

    MonkeyDevcie API 實踐全記錄

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI