
# 快速开始
本章节以开发Android客户端为例，帮助用户理解基础的Android客户端开发流程以及如何集成KooPhone Android端SDK，主要包含两个基本功能，发起云手机连接与断开云手机连接。本章节所有开发示例均为展示基本功能的实现，仅供参考。
#### 准备工作
准备开发Android客户端的环境/工具。
- 开发工具：Android Studio （推荐最新版本）
- JDK版本：\>= 1.8
- 最小SDK：API 23或更高
- 下载[KooPhone端aar SDK包](https://download.huaweicloud-koophone.com/app/cloudphone_android_sdk_26.3.0.aar)
 
 #### 步骤一 ：创建工程
1. 创建新工程。 
   1. 打开Android Studio，单击"New Project"。
   
   2. 选择"Empty Views Activity"模板，单击"Next"。
   
   3. 配置以下参数：
      - Name：CloudPhoneController
      
      - Package name：com.example. cloudphonecontroller
      
      - Language：选择Java
      
      - Minimum SDK：选择API 24或更高
       
   
   4. 配置完成后单击"Finish"，等待项目初始化完成。
   
   
   
   
2. 集成aar SDK（预置在指定目录）。 
   1. 通过下载[KooPhone端aar SDK包](https://download.huaweicloud-koophone.com/app/cloudphone_android_sdk_26.3.0.aar)并解压缩，获取KooPhone端aar SDK。
   
   2. 创建libs文件夹：如果项目根目录下没有libs文件夹，请在"Project"视图模式下，右键单击app模块，选择"New \> Directory"，输入"libs"并单击"确认"。
   
   3. 复制AAR文件：将下载好的aar SDK文件复制粘贴到[2.b]创建的app/libs目录下。
   
   
   
   
3. 配置项目依赖。 
   1. 打开app模块下的*build.gradle* 文件（*Module: app*）。
   
   2. 在android代码块中，添加sourceSets配置，确保Gradle能识别libs目录下的本地依赖。
      ```
      android {
          // ... 其他配置
          sourceSets {
              getByName("main") {
                  jniLibs.srcDirs("libs")
              }
          }
      ```
      
   
   3. 在dependenciesaar代码块中，添加对aar文件的引用。
      ```
      dependencies {
          
          implementation fileTree(dir: 'libs', include: '*.aar') //原有arr的依赖
          // 其他依赖
          implementation 'androidx.appcompat:appcompat:1.6.1'
          // ...
      }
      ```
      
   
   4. 单击编辑器右上角的 "Sync Now" 同步项目。
   
   
   
   
4. 准备基础配置。 
   1. 配置基础参数类。 项目中通过BuildConfig和Constant统一管理关键参数，开发者至少需要准备以下参数：
      - INSTANCE_ID：实例ID，用于获取实例连接信息。
        - 实例版：购买云手机（实例版）后， 可进入云手机控制台的实例版页面 ，在"云手机 \> 实例管理"中查看。
        
        - 节点版：购买云手机（节点版）后， 可进入云手机控制台的节点版页面 ，在"云手机 \> 资源管理 \> 实例"中查看。
         
      
      - SCREEN_WIDTH / SCREEN_HEIGHT：云侧目标分辨率。
      
      - USER_NAME / USER_PASSWORD / DOMAIN_NAME / PROJECT_NAME：IAM鉴权信息，可参考[获取账号、IAM用户、项目、用户组、区域、委托的名称和ID](https://support.huaweicloud.com/api-iam/iam_17_0002.html)填写对应参数。
      
      - IAM_BASE_URL ：IAM鉴权接口地址，可参考[认证鉴权](https://support.huaweicloud.com/api-koophone/kp_01_0005.html)填写对应参数。
      
      - API_BASE_URL：请求云手机服务的串流实例信息接口地址（https://koophone.myhuaweicloud.com/）。
      
      
      **Config.java**示例代码如下：
      ```
      public class Config {
          public static final String USER_ID = BuildConfig.USER_ID;
          public static final int SCREEN_WIDTH = BuildConfig.SCREEN_WIDTH;
          public static final int SCREEN_HEIGHT = BuildConfig.SCREEN_HEIGHT;
          
      }
      ```
      **Constant.java**示例代码如下：
      ```
      public class Constant { 
           public final static String INSTANCE_ID = com.example.cloudphonecontroller.BuildConfig.INSTANCE_ID; 
           // 请求IAM接口相关参数
           public final static String IAM_BASE_URL = ""; 
           public final static String USER_NAME = ""; 
           public final static String USER_PASSWORD = ""; 
           public final static String DOMAIN_NAME = ""; 
           public final static String PROJECT_NAME = ""; 
           // 请求业务接口相关参数
           public final static String API_BASE_URL = ""; 
       }
      ```
      USER_NAME、USER_PASSWORD、DOMAIN_NAME、PROJECT_NAME需要开发者替换为自己的租户账号信息。 INSTANCE_ID也必须提前配置，否则后续无法获取实例连接信息。
      
   
   2. 在Gradle中配置BuildConfig字段。 由于Config和Constant中会读取BuildConfig字段，因此还需要在app/build.gradle中补充如下配置：
      ```
      android {
          defaultConfig {
              buildConfigField "String", "INSTANCE_ID", "\"你的实例ID\""
              buildConfigField "int", "SCREEN_WIDTH", "1920"
              buildConfigField "int", "SCREEN_HEIGHT", "1080"
              buildConfigField "int", "AUDIO_SAMPLE_RATE", "48000"
              buildConfigField "int", "AUDIO_CHANNEL_COUNT", "2"
          }
      }
      ```
      
   
   
   
   
 
 #### 步骤二：获取鉴权信息
请求鉴权信息之前，需要做如下准备：
- 已购买KooPhone云手机产品。
- 已获取IAM租户鉴权信息，可参考[认证鉴权](https://support.huaweicloud.com/api-koophone/kp_01_0005.html)。
  注意：IAM租户鉴权信息需严格遵循官网指导文档开发意见获取，不可频繁调用，避免产生不必要的问题。
  
- 参考[租户实例串流前获取设备的device_token](https://support.huaweicloud.com/api-koophone/GetDeviceToken.html)，获取串流所需参数：
  - device_token：鉴权Token
  
  - device_id：设备ID
  
  - signaling_url：信令服务器地址
   
建议开发者严格按[图1]顺序实现：
图1客户端启动串流   
![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002561539637.png)
1. 准备请求与响应模型。 
   为了让Retrofit能正常请求并解析数据，需要先准备请求体和响应体模型。
   1. IAM请求模型AuthRequest。
      ```
      package com.example.cloudphonecontroller.model.request;
      public class AuthRequest {
          public Auth auth;
          public static class Auth {
              public Identity identity;
              public Scope scope;
          }
          public static class Identity {
              public String[] methods = {"password"};
              public Password password;
          }
          public static class Password {
              public User user;
          }
          public static class User {
              public Domain domain;
              public String name;
              public String password;
          }
          public static class Domain {
              public String name;
          }
          public static class Scope {
              public Project project;
          }
          public static class Project {
              public String name;
          }
      }
      ```
      
   
   2. IAM响应模型AuthResponse。
      ```
      package com.example.cloudphonecontroller.model.response;
      public class AuthResponse {
          public Token token;
          public static class Token {
              public String id;
              // 其他token字段
          }
          @Override
          public String toString() {
              return "AuthResponse{" +
                      "token=" + token +
                      '}';
          }
      }
      ```
      
   
   3. 业务接口响应模型ApiResponse。
      ```
      package com.example.cloudphonecontroller.model.response;
      public class ApiResponse {
          private Data data;
          private String error_code;
          private String error_msg;
          @Override
          public String toString() {
              return "ApiResponse{" +
                      "data=" + data +
                      ", error_code='" + error_code + '\'' +
                      ", error_msg='" + error_msg + '\'' +
                      '}';
          }
          public static class Data {
              private Resource resource;
              private String device_token;
              public static class Resource {
                  private Sdk sdk;
                  private Rtc rtc;
                  private String device_id;
                  private String kp_id;
                  public static class Sdk {
                      private Internal internal;
                      private External external;
                      @Override
                      public String toString() {
                          return "Sdk{" +
                                  "internal=" + internal +
                                  ", external=" + external +
                                  '}';
                      }
                      public static class Internal {
                          private String address;
                          private Integer aport;
                          private Integer atype;
                          private String address_ipv6;
                          // Getters and Setters
                          public String getAddress() { return address; }
                          public void setAddress(String address) { this.address = address; }
                          public Integer getAport() { return aport; }
                          public void setAport(Integer aport) { this.aport = aport; }
                          public Integer getAtype() { return atype; }
                          public void setAtype(Integer atype) { this.atype = atype; }
                          public String getAddress_ipv6() { return address_ipv6; }
                          public void setAddress_ipv6(String address_ipv6) { this.address_ipv6 = address_ipv6; }
                          @Override
                          public String toString() {
                              return "Internal{" +
                                      "address='" + address + '\'' +
                                      ", aport=" + aport +
                                      ", atype=" + atype +
                                      ", address_ipv6='" + address_ipv6 + '\'' +
                                      '}';
                          }
                      }
                      public static class External {
                          private String address;
                          private Integer aport;
                          private Integer atype;
                          private String address_ipv6;
                          private String address_adn;
                          // Getters and Setters
                          public String getAddress() { return address; }
                          public void setAddress(String address) { this.address = address; }
                          public Integer getAport() { return aport; }
                          public void setAport(Integer aport) { this.aport = aport; }
                          public Integer getAtype() { return atype; }
                          public void setAtype(Integer atype) { this.atype = atype; }
                          public String getAddress_ipv6() { return address_ipv6; }
                          public void setAddress_ipv6(String address_ipv6) { this.address_ipv6 = address_ipv6; }
                          public String getAddress_adn() {
                              return address_adn;
                          }
                          public void setAddress_adn(String address_adn) {
                              this.address_adn = address_adn;
                          }
                          @Override
                          public String toString() {
                              return "External{" +
                                      "address='" + address + '\'' +
                                      ", aport=" + aport +
                                      ", atype=" + atype +
                                      ", address_ipv6='" + address_ipv6 + '\'' +
                                      '}';
                          }
                      }
                      // Getters and Setters
                      public Internal getInternal() { return internal; }
                      public void setInternal(Internal internal) { this.internal = internal; }
                      public External getExternal() { return external; }
                      public void setExternal(External external) { this.external = external; }
                  }
                  public static class Rtc {
                      private IceSignaling ice_signaling;
                      @Override
                      public String toString() {
                          return "Rtc{" +
                                  "ice_signaling=" + ice_signaling +
                                  '}';
                      }
                      public static class IceSignaling {
                          private String signaling_url;
                          private String expired_time;
                          private List<Object> ice_servers;
                          // Getters and Setters
                          public String getSignaling_url() { return signaling_url; }
                          public void setSignaling_url(String signaling_url) { this.signaling_url = signaling_url; }
                          public String getExpired_time() { return expired_time; }
                          public void setExpired_time(String expired_time) { this.expired_time = expired_time; }
                          public List<Object> getIce_servers() { return ice_servers; }
                          public void setIce_servers(List<Object> ice_servers) { this.ice_servers = ice_servers; }
                          @Override
                          public String toString() {
                              return "IceSignaling{" +
                                      "signaling_url='" + signaling_url + '\'' +
                                      ", expired_time='" + expired_time + '\'' +
                                      ", ice_servers=" + ice_servers +
                                      '}';
                          }
                      }
                      // Getters and Setters
                      public IceSignaling getIce_signaling() { return ice_signaling; }
                      public void setIce_signaling(IceSignaling ice_signaling) { this.ice_signaling = ice_signaling; }
                  }
                  // Getters and Setters
                  public Sdk getSdk() { return sdk; }
                  public void setSdk(Sdk sdk) { this.sdk = sdk; }
                  public Rtc getRtc() { return rtc; }
                  public void setRtc(Rtc rtc) { this.rtc = rtc; }
                  public String getDevice_id() { return device_id; }
                  public void setDevice_id(String device_id) { this.device_id = device_id; }
                  public String getKp_id() { return kp_id; }
                  public void setKp_id(String kp_id) { this.kp_id = kp_id; }
                  @Override
                  public String toString() {
                      return "Resource{" +
                              "sdk=" + sdk +
                              ", rtc=" + rtc +
                              ", device_id='" + device_id + '\'' +
                              ", kp_id='" + kp_id + '\'' +
                              '}';
                  }
              }
              // Getters and Setters
              public Resource getResource() { return resource; }
              public void setResource(Resource resource) { this.resource = resource; }
              public String getDevice_token() { return device_token; }
              public void setDevice_token(String device_token) { this.device_token = device_token; }
              @Override
              public String toString() {
                  return "Data{" +
                          "resource=" + resource +
                          ", device_token='" + device_token + '\'' +
                          '}';
              }
          }
          // Getters and Setters
          public Data getData() { return data; }
          public void setData(Data data) { this.data = data; }
          public String getError_code() { return error_code; }
          public void setError_code(String error_code) { this.error_code = error_code; }
          public String getError_msg() { return error_msg; }
          public void setError_msg(String error_msg) { this.error_msg = error_msg; }
      }
      ```
      该类用于接收实例连接信息，开发者至少需要保证以下字段可解析成功：
      - device_token
      
      - external.address
      
      - external.aport
      
      - ice_signaling.signaling_url
       
   
   
   
   
2. 定义接口服务。定义接口服务后，项目就具备了请求IAM接口和业务接口的能力。 
   **ApiService.java**
   ```
   import io.reactivex.Observable;
   import retrofit2.Response;
   import retrofit2.http.Body;
   import retrofit2.http.Headers;
   import retrofit2.http.POST;
   import retrofit2.http.Path;
   import retrofit2.http.Query;
   public interface ApiService {
       /**
        * 获取华为云鉴权token
        *
        * @return
        */
       @POST("v3/auth/tokens")
       @Headers("Content-Type: application/json")
       public Observable<Response<ResponseBean<AuthResponse>>> authenticate(
               @Body AuthRequest authRequest,
               @Query("nocatalog") boolean noCatalog
       );
       /**
        * 获取串流信息
        *
        * @return
        */
       @POST("v1/instances/{instance_id}/auth")
       public Observable<ApiResponse> getDeviceInfo(
               @Path("instance_id") String id
       );
   }
   ```
   - **v3/auth/tokens**用于用户身份认证，向IAM服务获取访问令牌（Token）。
   
   - **v1/instances/{instance_id}/auth**用于获取指定实例的连接信息和串流参数。
   
   
   
   
3. 获取IAM Token。 
   项目中通过TokenManager.fetchToken()获取IAM Token（**IAM的调用面向管理者， token获取需要进行缓存**）。
   ```
   public static synchronized String fetchToken() throws IOException, RuntimeException {
       AuthRequest authRequest = new AuthRequest();
       authRequest.auth = new AuthRequest.Auth();
       authRequest.auth.identity = new AuthRequest.Identity();
       authRequest.auth.identity.password = new AuthRequest.Password();
       authRequest.auth.identity.password.user = new AuthRequest.User();
       authRequest.auth.identity.password.user.domain = new AuthRequest.Domain();
       authRequest.auth.identity.password.user.domain.name = Constant.DOMAIN_NAME;
       authRequest.auth.identity.password.user.name = Constant.USER_NAME;
       authRequest.auth.identity.password.user.password = Constant.USER_PASSWORD;
       authRequest.auth.scope = new AuthRequest.Scope();
       authRequest.auth.scope.project = new AuthRequest.Project();
       authRequest.auth.scope.project.name = Constant.PROJECT_NAME;
       Response<ResponseBean<AuthResponse>> responseBeanResponse =
               IAMServiceManager.createService(ApiService.class)
                       .authenticate(authRequest, true)
                       .blockingFirst();
       currentToken = responseBeanResponse.headers().get("X-Subject-Token");
       return currentToken;
   }
   ```
   
   
4. 在MainActivity中拉起鉴权流程。 
   获取IAM Token后，就可以在MainActivity的connectCloudPhone()方法中拉起完整鉴权流程获取云机实例的串流信息并校验关键字段，进入串流启动流程。
   ```
   private void connectCloudPhone() {
           // 检查连接状态
           if (isConnected) {
               showToast("已经连接");
               return;
           }
           tvStatus.setText("状态：正在获取鉴权信息...");
           // 在子线程中进行网络请求
           new Thread(() -> {
               try {
                   // 1.获取IAM Token
                   String token = TokenManager.fetchToken();
                   Log.info(TAG,"get token is : " + token);
                   //2.检查实例ID是否为空
                   if(Constant.INSTANCE_ID.isEmpty()){
                       this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "请在gradle.properties里填入 INSTANCE_ID", Toast.LENGTH_LONG).show());
                       return;
                   }
                   // 3.使用IAM Token和实例ID调用获取串流信息接口获取云机实例连接信息
                   Observable<ApiResponse> deviceInfo = KooPhoneServiceManager
                           .createService(ApiService.class, token)
                           .getDeviceInfo(Constant.INSTANCE_ID);
                   // 4.切换到主线程处理结果
                   runOnUiThread(() -> {
                       deviceInfo.subscribeOn(Schedulers.io())
                               .observeOn(AndroidSchedulers.mainThread())
                               .subscribe(new DataSubcriber<ApiResponse>() {
                                   @Override
                                   public void onNext(ApiResponse response) {
                                       Log.info(TAG,"ApiResponse " + response.toString());
                                       if (response == null || response.getData() == null ||
                                               response.getData().getDevice_token() == null) {
                                           tvStatus.setText("状态：鉴权失败");
                                           showToast("获取的Token为null，请检查实例状态");
                                           return;
                                       }
                                       // 5.启动串流
                                       startStream(response);
                                       tvStatus.setText("状态：已连接");
                                       isConnected = true;
                                       blackOverlay.setVisibility(View.GONE);
                                       tvStatus.setVisibility(View.GONE);
                                   }
                                   @Override
                                   public void onError(Throwable e) {
                                       tvStatus.setText("状态：鉴权失败");
                                       showToast("获取Token失败: " + e.getMessage());
                                   }
                               });
                   });
               } catch (Exception e) {
                   runOnUiThread(() -> {
                       tvStatus.setText("状态：连接失败");
                       showToast("连接异常: " + e.getMessage());
                   });
               }
           }).start();
       }
   ```
   拉起鉴权流程的执行顺序如下：
   1. 调用IAM接口获取Token。
   
   2. 校验实例ID是否为空。
   
   3. 使用IAM Token和实例ID调用获取串流信息接口获取云机实例连接信息。
   
   4. 接口返回成功后，需要校验关键字段，并进入串流启动流程。
   
   5. 校验成功进入串流流程。
   
   
   MainActivity中拉起鉴权后，客户端可以正常获取IAM Token、获取实例连接信息、在业务响应有效时进入串流启动流程。
   
   
 
 #### 步骤三：开发Demo及启动串流
开发MainActivity可以实现一个最小客户端的基础初始化，使应用具备正常启动页面、初始化SDK、初始化UI的能力。
onCreate是MainActivity创建时执行的核心方法，用于完成初始化工作。示例如下：
```
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // 初始化SDK
    initCloudPhoneSDK();
    // 初始化UI
    initUI();
}
```
1. 初始化SDK（初始化SDK后，客户端已经具备调用SDK能力，但还未开始串流）。 
   ```
   private void initCloudPhoneSDK() {
       CloudPhoneClient.init(this, null, true);
       CloudPhoneClient.playerForceTextureView(false);
   }
   ```
   - CloudPhoneClient.init(...)：初始化SDK。
   
   - playerForceTextureView(false)：设置播放器渲染模式。
   
   
   
   
2. 初始化页面承载容器。 
   串流画面需要有承载容器，本示例使用PlayerFragment作为串流承载对象，并在initUI()中完成绑定。初始化页面时，需要创建PlayerFragment并绑定到SDK，用于承载后续串流画面。
   ```
   private void initUI() { 
           //设置基础组件遮罩，状态信息，串流/断流按钮等；
           blackOverlay = findViewById(R.id.black_overlay);
           tvStatus = findViewById(R.id.tv_status);
           btnConnect = findViewById(R.id.btn_connect);
           btnDisconnect = findViewById(R.id.btn_disconnect);
           // 初始化PlayerFragment
           if (mFragment == null) {
               mFragment = new PlayerFragment();
               getFragmentManager()
                       .beginTransaction()
                       .add(R.id.containerf, mFragment, "player_fragment")
                       .commit();
               CloudPhoneClient.bindFragment(mFragment);
           }
           // 设置按钮监听
           btnConnect.setOnClickListener(v -> connectCloudPhone());
           btnDisconnect.setOnClickListener(v -> disconnectCloudPhone());
   }
   ```
   初始化页面承载容器时，可实现如下操作：
   - 设置基础组件按钮
   
   - 创建PlayerFragment
   
   - 将PlayerFragment添加到页面的containerf容器中
   
   - 调用CloudPhoneClient.bindFragment(m_fragment) ，绑定SDK与页面承载组件
   
   
   **如果缺少初始化页面承载容器这步操作，后续即使鉴权成功、串流启动成功，也无法正常显示画面。**
   
   
3. 发起云手机连接。 
   1. 构建串流启动参数。 在调用SDK启动串流前，需要构建连接所需参数（可自定义传入，参数在[步骤二：获取鉴权信息]中获取）。 这些参数通常包括：
      - 会话Token
      
      - 远端服务地址
      
      - 服务端口
      
      - 视频编码配置
      
      - 用户标识
      
      - 分辨率配置
      
      - 各类业务开关
      
      
      构建串流启动参数示例代码如下：
      ```
      private Bundle createBundle(ApiResponse authResponse)  {
              Bundle bundle = new Bundle();
              if (authResponse != null && null != authResponse.getData()) {
                  ApiResponse.Data data = authResponse.getData();
                  // 会话Token
                  bundle.putString(LAUNCH_KEY_TOKEN, TextUtils.isEmpty(data.getDevice_token()) ? "" : data.getDevice_token());
                  ApiResponse.Data.Resource resource = data.getResource();
                  if (null != resource) {
                      ApiResponse.Data.Resource.Sdk sdk = resource.getSdk();
                      if (null != sdk) {
                          ApiResponse.Data.Resource.Sdk.External external = sdk.getExternal();
                          if (null != external) {
                              //服务地址
                              bundle.putString(LAUNCH_KEY_ADDRESS, external.getAddress());
                              //服务端口
                              bundle.putInt(LAUNCH_KEY_PORT, external.getAport());
                          }
                      }
                  }
              }
              // 用户标识
              bundle.putString("userId", Config.USER_ID);
              // 自适应分辨率
              bundle.putBoolean("free_aspect", true);
              // 目标分辨率
              bundle.putInt("screen_width", Config.SCREEN_WIDTH);//任意分辨率宽
              bundle.putInt("screen_height", Config.SCREEN_HEIGHT);//任意分辨率高
              return bundle;
          }
      ```
      关键字段说明：
      - LAUNCH_KEY_TOKEN：接口返回的device_token
      
      - LAUNCH_KEY_ADDRESS：实例外网地址
      
      - LAUNCH_KEY_PORT：实例连接端口
      
      - userId：用户标识
      
      - screen_width / screen_height：分辨率
      
      - free_aspect：是否开启自由比例
      
      
      开发者在搭建demo时，至少要保证以上字段都被正确组装。
      注意事项：
      如果上一步获取的地址external.getAddress()为空，说明传递的是adn地址external.getAddress_adn()，需要先把adn域名解析为ip地址，具体方法如下：
      ```
      String ipAddress = external.getAddress();
      if (TextUtils.isEmpty(ipAddress)) {
          try {
              InetAddress inetAddress = InetAddress.getByName(external.getAddress_adn());
              if (null != inetAddress) {
                  ipAddress = inetAddress.getHostAddress();
              }
          } catch (Exception e) {
              throw new RuntimeException(e);
          }
      }
      ```
      
   
   2. 启动串流。 客户端在获取鉴权信息后，可以调用以下方法启动串流。
      ```
      private void startStream(ApiResponse response) {
              Bundle bundle = createBundle(response);
              CloudPhoneCallBack callBack = CloudPhoneCallBack.getInstance();
              EmulationTransport.init(this); 
              // 设置回调
              CloudPhoneClient.setPlayerCallback(callBack);
              CloudPhoneClient.setPlayerType(HMTP_PLAYER);
              // 启动串流
              CloudPhoneClient.start(mFragment, bundle);
          }
      ```
      
   
   
   
   
4. 关闭云手机连接。示例代码如下： 
   ```
   private void disconnectCloudPhone() {
           if (!isConnected) {
               showToast("当前未连接");
               return;
           }
           // 停止串流
           CloudPhoneClient.stop();
           isConnected = false;
           tvStatus.setText("状态：未连接");
           showToast("已断开连接");
           blackOverlay.setVisibility(View.VISIBLE); // 显示黑色遮罩
           tvStatus.setVisibility(View.VISIBLE);
       }
   ```
   
   
#### 客户端demo功能演示
- 初始化云机。
  1. 在MainActivity的onCreate()中初始化SDK和UI。
  
  2. 创建并绑定PlayerFragment作为画面容器。
  
  
  [图2]为初始化后的画面。
  图2初始化云机   
  ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002530457210.png "点击放大") 

- 连接云手机。在云手机中单击"连接云手机"，连接云手机后的界面如[图3]所示。
  连接云手机后，后台会执行如下操作：
  1. 调用IAM服务获取Token。
  
  2. 使用Token获取云手机连接信息。
  
  3. 构建串流参数Bundle。
  
  4. 调用SDK的start()方法启动串流。
  
  5. 隐藏遮罩层，显示云手机画面。
  
  
  图3连接云手机   
  ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002561537139.png "点击放大") 
- 断开云手机的连接。在云手机中单击"断开连接"，连接云手机后的界面如[图4]所示。
  断开连接云手机后，后台会执行如下操作：
  1. 调用SDK的stop()方法停止串流。
  
  2. 显示遮罩层。
  
  3. 更新连接状态。
  
  
  图4断开连接   
  ![](https://support.huaweicloud.com/devg-koophone/zh-cn_image_0000002530617208.png "点击放大") 
- 状态管理。
  - 通过TextView实时显示连接状态。
  
  - 使用Toast提示操作结果。
  
  - 通过遮罩层控制画面显示。
   
 
