更新时间:2026-05-22 GMT+08:00
代码示例
以下代码仅为示例之一, 用户也可通过调用其他API方法实现获取设备的device_token接口,可参考租户实例串流前获取设备的device_token,用于串流前身份鉴权。
工程目录结构代码示例
工程目录结构代码示例最小化实现了云手机串流、断流的能力。
CloudPhoneController/ ├── app/ # 应用主模块 │ ├── libs/ # 存放第三方库文件(如SDK的AAR文件) │ │ └── cloudphone-sdk.aar # 云手机SDK库文件 │ ├── src/ # 源代码目录 │ │ ├── main/ # 主代码目录 │ │ │ ├── java/ # Java源代码目录 │ │ │ │ ├── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── cloudphonecontroller/ │ │ │ │ │ ├── MainActivity.java # 主Activity,控制云手机连接/断开 │ │ │ │ │ ├── callback/ # 回调处理目录 │ │ │ │ │ │ └── CloudPhoneCallBack.java # SDK回调处理器 │ │ │ │ │ ├── constant/ # 常量定义目录 │ │ │ │ │ │ └── Constant.java # 应用常量定义 │ │ │ │ │ ├── config/ # 配置定义目录 │ │ │ │ │ │ └── Config.java # 配置文件 │ │ │ │ │ ├── model/ # 数据模型目录 │ │ │ │ │ │ ├── request/ # 请求模型 │ │ │ │ │ │ │ └── AuthRequest.java # IAM鉴权请求模型 │ │ │ │ │ │ └── response/ # 响应模型 │ │ │ │ │ │ ├── AuthResponse.java # IAM鉴权响应模型 │ │ │ │ │ │ └── ApiResponse.java # 业务接口响应模型 │ │ │ │ │ ├── network/ # 网络请求目录 │ │ │ │ │ │ ├── ApiService.java # 网络接口定义 │ │ │ │ │ │ ├── TokenManager.java # Token管理器 │ │ │ │ │ │ └── service/ # 服务管理 │ │ │ │ │ │ ├── IAMServiceManager.java # IAM服务管理 │ │ │ │ │ │ └── KooPhoneServiceManager.java # 云手机服务管理 │ │ │ │ │ └── util/ # 工具类目录 │ │ │ │ │ └── DataSubscriber.java # 数据订阅者工具 │ │ │ │ │ └──DefaultSubscriber.java # 数据订阅者抽象类 │ │ │ │ │ └── base/ │ │ │ │ │ │ └──ResponseBean.java # 统一接口返回数据封装类 │ │ │ │ │ │ └──BaseEntity.java # 公共基类 │ │ │ ├── res/ # 资源文件目录 │ │ │ │ ├── layout/ # 布局文件 │ │ │ │ │ ├── activity_main.xml # 主界面布局 │ │ │ │ └── values/ # 资源值 │ │ │ │ └── strings.xml # 字符串资源 │ │ │ └── AndroidManifest.xml # Android应用清单文件 │ │ └── build.gradle # 模块级构建配置 │ └── build.gradle # 应用级构建配置 └── gradle.properties # Gradle属性配置
关键文件实现代码示例:
- 模块级构建配置app/build.gradle代码示例:
apply plugin: 'com.android.application' android { compileSdkVersion 34 buildToolsVersion "30.0.0" buildFeatures { buildConfig true } defaultConfig { applicationId "com.example.cloudphonecontroller" minSdkVersion 28 targetSdkVersion 34 versionCode 1 versionName "1.0" renderscriptTargetApi 21 renderscriptSupportModeEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // 配置BuildConfig字段 buildConfigField "String", "INSTANCE_ID", "\"your_instance_id\"" buildConfigField "String", "USER_ID", "\"43740902807\"" buildConfigField "int", "SCREEN_WIDTH", "1920" buildConfigField "int", "SCREEN_HEIGHT", "1080" buildConfigField "int", "AUDIO_SAMPLE_RATE", "48000" buildConfigField "int", "AUDIO_CHANNEL_COUNT", "2" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } buildFeatures { viewBinding true } sourceSets { getByName("main") { jniLibs.srcDirs("libs") } } } ext { versions = [appcompat : "1.7.1", activity : "1.9.3", constraintlayout: "2.2.1", material : "1.7.0", preference : "1.1.1", androidx_test : "1.2.0", junit : "1.1.1", espresso : "3.2.0", mockito : "5.14.2",] } dependencies { implementation platform("org.jetbrains.kotlin:kotlin-bom:1.8.10") implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation("androidx.appcompat:appcompat:${versions.appcompat}") implementation "com.google.android.material:material:${versions.material}" implementation "androidx.activity:activity:${versions.activity}" implementation "androidx.constraintlayout:constraintlayout:${versions.constraintlayout}" // 测试相关 testImplementation "junit:junit:4.13.2" androidTestImplementation "androidx.test.ext:junit:${versions.junit}" androidTestImplementation "androidx.test.espresso:espresso-core:${versions.espresso}" implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "com.squareup.retrofit2:adapter-rxjava2:2.9.0" implementation "io.reactivex.rxjava2:rxjava:2.2.21" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" // 其他依赖 implementation "com.fasterxml.jackson.core:jackson-databind:2.12.6" implementation 'org.greenrobot:eventbus:3.3.1' implementation "androidx.window:window:1.2.0" implementation "androidx.window:window-java:1.2.0" // Lombok compileOnly "org.projectlombok:lombok:1.18.30" annotationProcessor "org.projectlombok:lombok:1.18.30" // Glide implementation "com.github.bumptech.glide:glide:4.16.0" annotationProcessor "com.github.bumptech.glide:compiler:4.16.0" implementation "com.github.bumptech.glide:okhttp3-integration:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" implementation "androidx.startup:startup-runtime:1.1.1" } configurations.all { resolutionStrategy { force "com.google.android.material:material:1.6.1" force "androidx.appcompat:appcompat:1.6.1" } } - Android应用清单文件AndroidManifest.xml代码示例:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> <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/Theme.CloudPhoneController" android:appComponentFactory="androidx.core.app.CoreComponentFactory" tools:replace="android:appComponentFactory,android:allowBackup"> <activity android:name=".MainActivity" android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> - 主界面布局activity_main.xml代码示例:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 云手机画面容器 --> <FrameLayout android:id="@+id/containerf" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 未连接时的遮罩层 --> <View android:id="@+id/black_overlay" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FF000000" android:alpha="1" /> <!-- 控制按钮 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:orientation="horizontal" android:padding="16dp" android:gravity="center"> <Button android:id="@+id/btn_connect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="连接云手机" android:textAllCaps="false" android:paddingHorizontal="24dp" android:paddingVertical="12dp"/> <Button android:id="@+id/btn_disconnect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="断开连接" android:textAllCaps="false" android:paddingHorizontal="24dp" android:paddingVertical="12dp" android:layout_marginStart="16dp"/> </LinearLayout> <!-- 状态显示 --> <TextView android:id="@+id/tv_status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="状态:未连接" android:textColor="@android:color/white" android:textSize="18sp" /> </RelativeLayout> - 主Activity,控制云手机连接/断开MainActivity.java代码示例:
package com.example.cloudphonecontroller; import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_ADDRESS; import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_PORT; import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_TOKEN; import static com.nbc.acsdk.adapter.IPlayer.HMTP_PLAYER; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.cloudphone.client.api.CloudPhoneClient; import com.example.cloudphonecontroller.callback.CloudPhoneCallBack; import com.example.cloudphonecontroller.config.Config; import com.example.cloudphonecontroller.constant.Constant; import com.example.cloudphonecontroller.model.response.ApiResponse; import com.example.cloudphonecontroller.network.ApiService; import com.example.cloudphonecontroller.network.KooPhoneServiceManager; import com.example.cloudphonecontroller.network.TokenManager; import com.example.cloudphonecontroller.util.DataSusbcriber; import com.google.gson.Gson; import com.nbc.acsdk.widget.PlayerFragment; import com.nbc.utils.Log; import java.io.File; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; public class MainActivity extends AppCompatActivity { private static Gson gson; private View blackOverlay; private TextView tvStatus; private Button btnConnect, btnDisconnect; private PlayerFragment mFragment; private boolean isConnected = false; private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化SDK initCloudPhoneSDK(); Log.info(TAG,"从这里开始 MainActivity onCreate"); // 初始化UI initUI(); } private void initCloudPhoneSDK() { // 初始化SDK CloudPhoneClient.init(this, null, true); // 设置播放渲染模式 CloudPhoneClient.playerForceTextureView(false); } 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()); } // 连接云手机功能 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); if(Constant.INSTANCE_ID.isEmpty()){ this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "请在gradle.properties里填入 INSTANCE_ID", Toast.LENGTH_LONG).show()); return; } // 2.使用IAM Token获取设备连接信息 Observable<ApiResponse> deviceInfo = KooPhoneServiceManager .createService(ApiService.class, token) .getDeviceInfo(Constant.INSTANCE_ID); // 3.切换到主线程处理结果 runOnUiThread(() -> { deviceInfo.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DataSubscriber<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; } // 4.启动串流 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()); Log.info(TAG,"获取Token失败: " + e.getMessage()); } }); }); } catch (Exception e) { runOnUiThread(() -> { tvStatus.setText("状态:连接失败"); showToast("连接异常: " + e.getMessage()); }); } }).start(); } 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); } 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; } // 断开云手机连接 private void disconnectCloudPhone() { if (!isConnected) { showToast("当前未连接"); return; } // 停止串流 CloudPhoneClient.stop(); isConnected = false; tvStatus.setText("状态:未连接"); showToast("已断开连接"); blackOverlay.setVisibility(View.VISIBLE); // 显示黑色遮罩 tvStatus.setVisibility(View.VISIBLE); } private void showToast(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } }
- SDK回调处理器CloudPhoneCallBack.java代码示例:
package com.example.cloudphonecontroller.callback; import com.cloudphone.client.api.CloudPhoneClient; public class CloudPhoneCallBack implements CloudPhoneClient.Callback, CloudPhoneClient.Callback.AppCallback { private static volatile CloudPhoneCallBack instance; private CloudPhoneCallBack() {} public static CloudPhoneCallBack getInstance() { if (instance == null) { synchronized (CloudPhoneCallBack.class) { if (instance == null) { instance = new CloudPhoneCallBack(); } } } return instance; } @Override public void onSuccess() { // 串流建立成功 EmulationTransport.getInstance().initTransport(); } @Override public void onFailure(int code, String msg) { // 串流建立失败 } } - 应用常量定义Constant.java代码示例:
package com.example.cloudphonecontroller.constant; import com.example.cloudphonecontroller.BuildConfig; public class Constant { public static final String INSTANCE_ID = BuildConfig.INSTANCE_ID; public static final String USER_ID = BuildConfig.USER_ID; // IAM鉴权信息 public static final String IAM_BASE_URL = "https://your-iam-endpoint.com/"; public static final String USER_NAME = "your_username"; public static final String USER_PASSWORD = "your_password"; public static final String DOMAIN_NAME = "your_domain"; public static final String PROJECT_NAME = "your_project"; // 业务接口地址 public static final String API_BASE_URL = "https://your-api-endpoint.com/"; } - Token管理器TokenManager.java代码示例:
package com.example.cloudphonecontroller.network; import com.example.cloudphonecontroller.network.ApiService; import com.example.cloudphonecontroller.base.ResponseBean; import com.example.cloudphonecontroller.constant.Constant; import com.example.cloudphonecontroller.model.request.AuthRequest; import com.example.cloudphonecontroller.model.response.AuthResponse; import java.io.IOException; import retrofit2.Response; public class TokenManager { private static String currentToken; // 同步获取 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; } } - IAM服务管理IAMServiceManager.java代码示例:
import com.example.cloudphonecontroller.constant.Constant; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Protocol; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; public class IAMServiceManager { private static Retrofit retrofit = null; private static String baseUrl = Constant.IAM_BASE_URL; public static <T> T createService(Class<T> serviceClass) { Retrofit retrofit = initRetrofit(); return retrofit.create(serviceClass); } private static synchronized Retrofit initRetrofit() { if (retrofit != null) { return retrofit; } OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.connectTimeout(15, TimeUnit.SECONDS) // 连接超时时间设置 .readTimeout(15, TimeUnit.SECONDS) // 读取超时时间设置 .protocols(Collections.unmodifiableList(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))); // 初始化Retrofit retrofit = new Retrofit.Builder() .client(builder.build()) .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); return retrofit; } } - 云手机服务管理KooPhoneServiceManager.java代码示例:
import com.example.cloudphonecontroller.constant.Constant; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import retrofit2.Retrofit; import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; public class KooPhoneServiceManager { private static Retrofit retrofit = null; private static String baseUrl = Constant.API_BASE_URL; private static String m_iamtoken = ""; public static <T> T createService(Class<T> serviceClass,String iamtoken) { m_iamtoken = iamtoken; retrofit = initRetrofit(); return retrofit.create(serviceClass); } public static synchronized Retrofit initRetrofit() { if (retrofit != null) { return retrofit; } OkHttpClient.Builder builder = new OkHttpClient.Builder(); try { final TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; } } }; // 初始化SSLContext SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustAllCerts, new SecureRandom()); // 获取SSLSocketFactory SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); // 配置OkHttp使用自定义的SSL builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]); // 跳过主机名验证 builder.hostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; // 信任所有主机名 } }); } catch (Exception e) { e.printStackTrace(); } builder.addInterceptor(chain -> { Request original = chain.request(); Request.Builder requestBuilder = original.newBuilder(); if (!original.url().toString().contains("/login")) { requestBuilder.header("x-auth-token", m_iamtoken); } return chain.proceed(requestBuilder.build()); }) .connectTimeout(15, TimeUnit.SECONDS) // 连接超时时间设置 .readTimeout(15, TimeUnit.SECONDS) // 读取超时时间设置 .protocols(Collections.unmodifiableList(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))); // 初始化Retrofit retrofit = new Retrofit.Builder() .client(builder.build()) .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build(); return retrofit; } } - 数据订阅者工具DataSubscriber.java代码示例:
import io.reactivex.disposables.Disposable; public abstract class DataSubscriber<T> extends DefaultSubscriber<T> { public DataSubcriber() { super(); } @Override public void onSubscribe(Disposable d) { } @Override public void onComplete() { } @Override public void onError(Throwable e) { } } - 数据订阅者抽象类DefaultSubscriber.java代码示例:
package com.example.cloudphonecontroller.util; import io.reactivex.Observer; public abstract class DefaultSubscriber<T> implements Observer<T> { } - 公共基类BaseEntity.Java代码示例:
package com.example.cloudphonecontroller.base; import java.io.Serializable; public class BaseEntity implements Serializable { }
- 统一接口返回数据封装类ResponseBean.java代码示例
package com.example.cloudphonecontroller.base; public class ResponseBean<T> extends BaseEntity { private boolean success; private String code; private String msg; private T data; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public boolean getSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } }
关键代码示例
sample/ ├── app/ │ ├── src/main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ ├── cloud/keyfunction/ │ │ │ │ ├── MainActivity.java # 主流程入口(初始化 + 串流启动) │ │ │ │ └── Config.java # 基础运行参数配置 │ │ │ │ │ │ │ └── KooCabin/ │ │ │ ├── api/ │ │ │ │ └── ApiService.java # 接口定义(IAM / 业务接口) │ │ │ │ │ │ │ ├── bean/ │ │ │ │ ├── request/ │ │ │ │ │ └── AuthRequest.java │ │ │ │ └── response/ │ │ │ │ ├── AuthResponse.java │ │ │ │ └── ApiResponse.java │ │ │ │ │ │ │ ├── constant/ │ │ │ │ └── Constant.java │ │ │ │ │ │ │ ├── iInterface/ │ │ │ │ └── CloudPhoneCallBack.java # SDK回调 │ │ │ │ │ │ │ ├── manager/ │ │ │ │ ├── TokenManager.java │ │ │ │ ├── IAMServiceManager.java │ │ │ │ └── KooPhoneServiceManager.java │ │ │ │ │ │ │ ├── subscriber/ │ │ │ │ ├── DataSubcriber.java │ │ │ │ └── DefualtSubscriber.java │ │ │ │ │ │ │ └── ui/base/ │ │ │ ├── BaseEntity.java │ │ │ └── ResponseBean.java │ │ │ │ │ ├── res/ │ │ │ ├── layout/ │ │ │ │ └── activity_main.xml │ │ │ ├── drawable/ │ │ │ ├── mipmap*/ │ │ │ ├── values/ │ │ │ └── xml/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── libs/ # SDK aar 包 │ └── build.gradle │ ├── build.gradle ├── settings.gradle └── gradle.properties
MainActivity.java
package com.cloud.keyfunction;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_EVENT_EXECUTOR;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_EVENT_INTERVAL_PROCESS_MS;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_HIGH_PERF_FRAME;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_LAUNCH_KEY_VENC_TYPE;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_LAUNCH_SENSOR_INTERVAL;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_TOUCH_INPUT_METHOD;
import static com.cloudapp.client.api.CloudAppConst.CLOUD_APP_USE_PROXY_SERVER;
import static com.cloudphone.client.api.CloudPhoneConst.CLOUD_APP_AUDIO_MAX_BUFFER_SIZE;
import static com.cloudphone.client.api.CloudPhoneConst.CLOUD_APP_HIDE_STREAM_AT_START_UP;
import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_ADDRESS;
import static com.cloudphone.client.api.CloudPhoneConst.CLOUD_APP_LAUNCH_KEY_AUDIO_ENCODE_TYPE;
import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_PORT;
import static com.cloudphone.client.api.CloudPhoneConst.CLOUD_APP_LAUNCH_KEY_PROFILE_LEVEL;
import static com.cloudphone.client.api.CloudPhoneConst.LAUNCH_KEY_TOKEN;
import static com.cloudphone.client.api.CloudPhoneConst.PROFILE_SPEED;
import static com.nbc.acsdk.adapter.IPlayer.HMTP_PLAYER;
import android.app.Dialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import com.KooCabin.subscriber.DataSubcriber;
import com.nbc.utils.Log;
import android.util.Base64;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.KooCabin.api.ApiService;
import com.KooCabin.bean.response.ApiResponse;
import com.KooCabin.constant.Constant;
import com.KooCabin.iInterface.CloudPhoneCallBack;
import com.KooCabin.manager.KooPhoneServiceManager;
import com.KooCabin.manager.TokenManager;
import com.cloudphone.client.api.CloudPhoneClient;
import com.cloudphone.client.api.CloudPhoneConst;
import com.cloudphone.client.bean.AppInfo;
import com.cloudphone.client.bean.AppOperateResponse;
import com.cloudphone.client.bean.AppTypeEnum;
import com.cloudphone.client.bean.GetAppsReq;
import com.cloudphone.client.bean.GetAppsRsp;
import com.nbc.acsdk.adapter.IPlayer;
import com.nbc.acsdk.hmtp.HmtpPlayerWrapper;
import com.nbc.acsdk.media.play.AudioPlayerConfig;
import com.nbc.acsdk.widget.PlayerFragment;
import java.io.File;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ArrayList<Button> appButtonList = new ArrayList<>();
private Map<Button,String> appButtonMap = new HashMap();
private Map<Button,String> sysButtonMap = new HashMap();
Button btnBackMain;
Button btnCloseApp;
Button btnTaskMng;
Button btnExit;
View frontView;
private String currentRunningApp = "";
PlayerFragment m_fragment = null;
boolean streamStarted = false;
// private Handler audioHandler;
private ExecutorService threadPool;
private CompositeDisposable disposables;
private static MainActivity instance;
private static final int AUDIO_DELAY_MS = 100;
//是否是合一版本,串流时bundle参数不一样
boolean isCombinedVersion = true;
public static MainActivity getInstance() {
return instance;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
instance = this;
// 初始化资源管理器
// audioHandler = new Handler(Looper.getMainLooper());
threadPool = Executors.newFixedThreadPool(3);
disposables = new CompositeDisposable();
// 初始化SDK
initCloudPhoneSDK();
Log.info(TAG,"从这里开始 MainActivity onCreate");
// 初始化UI
initUI();
// 开始连接云手机
fetchTokenAndStartSteam();
}
private void initCloudPhoneSDK() {
CloudPhoneClient.init(this, null, true);
CloudPhoneClient.playerForceTextureView(false);
CloudPhoneClient.setLogFolder(this.getExternalFilesDir("log").getPath() + File.separator);
}
private void initUI() {
frontView = findViewById(R.id.black_overlay);
findAppButtonAndAdd2Map();
findSysButtonAndAdd2Map();
setSysButtonClick();
if (m_fragment == null) {
m_fragment = new PlayerFragment();
getFragmentManager()
.beginTransaction()
.add(R.id.containerf, m_fragment, "player_fragment")
.commit();
CloudPhoneClient.bindFragment(m_fragment);
}
setAppButtonVisible(false);
}
@Override
protected void onStart() {
super.onStart();
Log.info(TAG,"MainActivity onStart");
}
@Override
protected void onStop() {
super.onStop();
Log.info(TAG,"MainActivity onStop");
}
@Override
protected void onPause() {
super.onPause();
Log.info(TAG,"MainActivity onPause");
}
@Override
protected void onResume() {
super.onResume();
Log.info(TAG,"MainActivity onResume");
}
@Override
protected void onDestroy() {
Log.info(TAG,"MainActivity.onDestroy");
// 清理所有资源
cleanupResources();
super.onDestroy();
}
/**
* 找到各app按钮并添加到Map中
*/
void findAppButtonAndAdd2Map(){
Button btnStartApp1 = findViewById(R.id.btnStartApp1);
Button btnStartApp2 = findViewById(R.id.btnStartApp2);
Button btnStartApp3 = findViewById(R.id.btnStartApp3);
Button btnStartApp4 = findViewById(R.id.btnStartApp4);
Button btnStartApp5 = findViewById(R.id.btnStartApp5);
Button btnStartApp6 = findViewById(R.id.btnStartApp6);
Button btnStartApp7 = findViewById(R.id.btnStartApp7);
Button btnStartApp8 = findViewById(R.id.btnStartApp8);
Button btnStartApp9 = findViewById(R.id.btnStartApp9);
Button btnStartApp10 = findViewById(R.id.btnStartApp10);
Button btnStartApp11 = findViewById(R.id.btnStartApp11);
Button btnStartApp12 = findViewById(R.id.btnStartApp12);
Button btnStartApp13 = findViewById(R.id.btnStartApp13);
Button btnStartApp14 = findViewById(R.id.btnStartApp14);
Button btnStartApp15 = findViewById(R.id.btnStartApp15);
Button btnStartApp16 = findViewById(R.id.btnStartApp16);
Button btnStartApp17 = findViewById(R.id.btnStartApp17);
Button btnStartApp18 = findViewById(R.id.btnStartApp18);
Button btnStartApp19 = findViewById(R.id.btnStartApp19);
Button btnStartApp20 = findViewById(R.id.btnStartApp20);
Button btnStartApp21 = findViewById(R.id.btnStartApp21);
Button btnTop = findViewById(R.id.btn_top);
btnTop.setText("顶部留空 当前使用的实例ID: "+Constant.INSTANCE_ID);
appButtonList.add(btnStartApp1);
appButtonList.add(btnStartApp2);
appButtonList.add(btnStartApp3);
appButtonList.add(btnStartApp4);
appButtonList.add(btnStartApp5);
appButtonList.add(btnStartApp6);
appButtonList.add(btnStartApp7);
appButtonList.add(btnStartApp8);
appButtonList.add(btnStartApp9);
appButtonList.add(btnStartApp10);
appButtonList.add(btnStartApp11);
appButtonList.add(btnStartApp12);
appButtonList.add(btnStartApp13);
appButtonList.add(btnStartApp14);
appButtonList.add(btnStartApp15);
appButtonList.add(btnStartApp16);
appButtonList.add(btnStartApp17);
appButtonList.add(btnStartApp18);
appButtonList.add(btnStartApp19);
appButtonList.add(btnStartApp20);
appButtonList.add(btnStartApp21);
}
/**
* 找到返回主屏、关闭应用、重启云机、退出云机按钮 并添加到Map中
*/
void findSysButtonAndAdd2Map(){
btnBackMain = findViewById(R.id.btnBackMain);
btnCloseApp = findViewById(R.id.btnCloseApp);
btnTaskMng= findViewById(R.id.btnTaskMng);
btnExit = findViewById(R.id.btnExit);
sysButtonMap.put(btnBackMain,"");
sysButtonMap.put(btnCloseApp,"");
sysButtonMap.put(btnTaskMng,"");
sysButtonMap.put(btnExit,"");
}
/**
* 设置单击各app按钮后的触发的事件
*/
void setAppButtonClick() {
for (Map.Entry<Button, String> entry : appButtonMap.entrySet()) {
final Button button = entry.getKey();
final String pkgname = entry.getValue();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchAppWithAudio(pkgname);
}
});
}
}
/**
* 设置launchAppWithAudio
*/
private void launchAppWithAudio(String packageName) {
Log.info(TAG, "调用SDK 启动应用<" + packageName + ">");
CloudPhoneClient.startApp(packageName);
currentRunningApp = packageName;
// startAudio();
CloudPhoneClient.switchStreamType(CloudPhoneConst.CLOUD_APP_SWITCH_STREAM_TYPE.BOTH);
showFragment();
}
/**
* 设置单击返回主屏、关闭应用、重启云机、退出云机按钮后的触发的事件
*/
void setSysButtonClick(){
btnBackMain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.info(TAG,"返回主屏 按钮被单击了");
CloudPhoneClient.sendKeyEvent(KeyEvent.KEYCODE_HOME);
CloudPhoneClient.switchStreamType(CloudPhoneConst.CLOUD_APP_SWITCH_STREAM_TYPE.AUDIO);
showAppWall();
}
});
btnCloseApp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.info(TAG,"关闭应用 按钮被单击了 "+currentRunningApp);
CloudPhoneClient.switchStreamType(CloudPhoneConst.CLOUD_APP_SWITCH_STREAM_TYPE.DATA);
if(!currentRunningApp.isEmpty())
{
CloudPhoneClient.closeApp(currentRunningApp);
}else{
Log.error(TAG,"currentRunningApp isEmpty ");
}
showAppWall();
}
});
btnTaskMng.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.info(TAG,"触发任务管理器");
GetAppsReq getAppsReq = new GetAppsReq();
getAppsReq.setAppType(AppTypeEnum.THIRD_APP);
getAppsReq.setNeedIcon(true);
getAppsReq.setPageNum(1);
getAppsReq.setPageSize(20);
getAppsReq.setQuality(70);
CloudPhoneClient.getRunningApps(getAppsReq);
}
});
btnExit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.info(TAG,"退出云机 ");
System.exit(0);
}
});
}
/**
* //展示应用墙界面
*/
private void showAppWall(){
frontView.setVisibility(View.VISIBLE);
setAppButtonVisible(true);
findViewById(R.id.btn_top).setVisibility(View.VISIBLE);
Log.info(TAG,"显示应用墙页面");
}
/**
* //展示串流界面
*/
private void showFragment(){
frontView.setVisibility(View.INVISIBLE);
setAppButtonVisible(false);
findViewById(R.id.btn_top).setVisibility(View.INVISIBLE);
Log.info(TAG,"显示串流页面");
}
void setAppButtonVisible(boolean b){
for(int i=0;i<appButtonList.size();i++)
{
appButtonList.get(i).setVisibility(View.INVISIBLE);
}
for (Map.Entry<Button, String> entry : appButtonMap.entrySet()) {
final Button button = entry.getKey();
if(b){
button.setVisibility(View.VISIBLE);
}else{
button.setVisibility(View.INVISIBLE);
}
}
}
private void fetchTokenAndStartSteam() {
threadPool.execute(() -> {
try {
if(Constant.INSTANCE_ID.isEmpty()){
this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "请在gradle.properties里填入 INSTANCE_ID", Toast.LENGTH_LONG).show());
return;
}
String result= TokenManager.fetchToken(); //拿到IAMtoken
if(result.equals(""))
{
Log.error(TAG, "拿到IAMtoken 为空 ");
}
Observable<ApiResponse> deviceInfo = KooPhoneServiceManager.createService(ApiService.class,result).getDeviceInfo(Constant.INSTANCE_ID); //拿到DeviceToken
executeAuthTokenAndStartStream(deviceInfo);
} catch (Exception e) {
Log.error(TAG, "Token initialization failed", e);
}
});
}
private void executeAuthTokenAndStartStream(Observable<ApiResponse> deviceInfo) {
// 订阅authToken接口
deviceInfo.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DataSubcriber<ApiResponse>() {
@Override
public void onNext(ApiResponse authResponse) {
if(authResponse.getData()==null||authResponse.getData().getDevice_token()==null||authResponse.getData().getResource()==null||
authResponse.getData().getResource().getRtc().getIce_signaling().getSignaling_url()==null)
{
MainActivity.this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "获取的Token等信息为null 请检查云机状态", Toast.LENGTH_LONG).show());
return;
}
Log.info(TAG,"S1------------- 成功获取token");
startStream(authResponse);
}
@Override
public void onError(Throwable e) {
Log.info(TAG,"onError "+e.toString());
MainActivity.this.runOnUiThread(() -> Toast.makeText(getApplicationContext(), "获取Token失败 请检查网络配置及云机ID是否正确 "+e.toString(), Toast.LENGTH_LONG).show());
}
@Override
public void onComplete() {
}
});
}
public void startStream(ApiResponse authResponseResponseBean) {
Bundle bundle = createBundle(authResponseResponseBean);
CloudPhoneCallBack callBack = CloudPhoneCallBack.getInstance();
CloudPhoneClient.setPlayerType(HMTP_PLAYER);
CloudPhoneClient.enableAudioKeeping(true);
CloudPhoneClient.setPlayerCallback(callBack);
CloudPhoneClient.setAppCallback(callBack);
CloudPhoneClient.start(null, bundle); //合一版本 使用null
Log.info(TAG,"调用SDK 初始化数据通道串流");
}
/**
* 创建重连所需的Bundle(需根据项目实际逻辑实现)
*/
private Bundle createBundle(ApiResponse authResponse) {
Bundle bundle = new Bundle();
if (authResponse != null && null != authResponse.getData()) {
ApiResponse.Data data = authResponse.getData();
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.putInt(CLOUD_APP_LAUNCH_KEY_VENC_TYPE, 0);
bundle.putInt(CLOUD_APP_LAUNCH_KEY_PROFILE_LEVEL, PROFILE_SPEED);
bundle.putString("userId", Config.USER_ID);
bundle.putBoolean(CLOUD_APP_HIDE_STREAM_AT_START_UP, true);
bundle.putBoolean("free_aspect", true);
bundle.putInt("screen_width", Config.SCREEN_WIDTH);//任意分辨率宽
bundle.putInt("screen_height", Config.SCREEN_HEIGHT);//任意分辨率高
//合一版本
if(isCombinedVersion)
{
bundle.putInt(CLOUD_APP_EVENT_EXECUTOR, 1); //ignition策略
bundle.putInt(CLOUD_APP_TOUCH_INPUT_METHOD,1);//触控方式
bundle.putInt(CLOUD_APP_EVENT_INTERVAL_PROCESS_MS,4);//mright间隔
bundle.putBoolean(CLOUD_APP_USE_PROXY_SERVER,false);//代理
bundle.putBoolean(CLOUD_APP_HIGH_PERF_FRAME,false);//补帧策略
bundle.putInt(CLOUD_APP_LAUNCH_SENSOR_INTERVAL,1000);//传感器间隔再把这坨加上
//aac需求
bundle.putInt(CLOUD_APP_AUDIO_MAX_BUFFER_SIZE, 19200*8);
bundle.putInt(CLOUD_APP_LAUNCH_KEY_AUDIO_ENCODE_TYPE, 0); // OPUS - 0 / PCM - 1
}
return bundle;
}
public void setStreamStarted(){
if(!streamStarted){
Log.info(TAG, "首次streamStarted 调getInstalledApps尝试获取应用列表");
GetAppsReq getAppsReq = new GetAppsReq();
getAppsReq.setAppType(AppTypeEnum.THIRD_APP);
getAppsReq.setNeedIcon(false);
getAppsReq.setPageNum(1);
getAppsReq.setPageSize(20);
getAppsReq.setQuality(85);
CloudPhoneClient.getInstalledApps(getAppsReq);
}
streamStarted = true;
}
public void updateAppList(AppOperateResponse<GetAppsRsp> appOperateResponse) {
Log.info(TAG, "处理已安装应用列表 显示按钮");
int count = appOperateResponse.getData().getTotalCount();
for(int i=0;i<count&&i<20;i++) //超20个会报错,先仅支持20个,后续扩展
{
AppInfo ai = appOperateResponse.getData().getAppList().get(i);
Log.info(TAG, "包名及应用名 "+ai.getPackageName() + " "+ai.getAppName());
appButtonList.get(i).setText(ai.getAppName());
appButtonMap.put(appButtonList.get(i),ai.getPackageName());
}
this.setAppButtonClick();
showAppWall();
}
//触发任务管理,获取到当前正在运行的应用列表后,进行显示
public void setRunningApps(AppOperateResponse<GetAppsRsp> appOperateResponse){
if (!appOperateResponse.getData().getAppList().isEmpty()) {
Log.info("onGetRunningApps", "遍历返回的list size=" +appOperateResponse.getData().getAppList().size());
for(int i=0;i<appOperateResponse.getData().getAppList().size();i++)
{
Log.info("onGetRunningApps", " appName: "+appOperateResponse.getData().getAppList().get(i).getAppName()
+" packageName: "+appOperateResponse.getData().getAppList().get(i).getPackageName()
+ " iconBytes: "+appOperateResponse.getData().getAppList().get(i).getIconBytes().length()
);
}
}
showImages(appOperateResponse);
}
private void showImages(AppOperateResponse<GetAppsRsp> appOperateResponse) {
// 创建全屏Dialog
Dialog dialog = new Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
// 创建垂直布局
LinearLayout mainLayout = new LinearLayout(this);
mainLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setBackgroundColor(0xFF000000);
// 添加关闭按钮
Button closeBtn = new Button(this);
closeBtn.setText("← 任务管理");
closeBtn.setOnClickListener(v -> dialog.dismiss());
// 设置按钮样式
closeBtn.setTextColor(0xFFFFFFFF);
closeBtn.setBackgroundColor(0xFFFF0000);
LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
btnParams.setMargins(0, 0, 0, 20);
closeBtn.setLayoutParams(btnParams);
mainLayout.addView(closeBtn);
// 创建两行水平布局
LinearLayout row1 = new LinearLayout(this);
row1.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout row2 = new LinearLayout(this);
row2.setOrientation(LinearLayout.HORIZONTAL);
// 设置行布局参数
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0
);
rowParams.weight = 1;
rowParams.setMargins(0, 0, 0, 10);
row1.setLayoutParams(rowParams);
row2.setLayoutParams(rowParams);
// 获取屏幕宽度
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenWidth = displayMetrics.widthPixels;
// 计算图片宽度(一行3个,减去间距后除以3)
int imageWidth = (screenWidth - 4 * 20) / 3; // 4个间距,每个20dp
// 根据16:10的比例计算高度
int imageHeight = (int) (imageWidth * 10.0f / 16.0f);
// 应用名数组
String[] appNames = new String[appOperateResponse.getData().getAppList().size()];
for(int i=0;i<appNames.length;i++)
{
appNames[i] = appOperateResponse.getData().getAppList().get(i).getAppName();
}
// 引用图标Base64数组(假设是小的引用图标)
// String[] iconBase64Images = {"图标1base64", "图标2base64", "图标3base64",
// "图标4base64", "图标5base64", "图标6base64"};
String[] iconBase64Images = new String[appOperateResponse.getData().getAppList().size()];
for(int i=0;i<iconBase64Images.length;i++)
{
iconBase64Images[i] = appOperateResponse.getData().getAppList().get(i).getIconBytes();
}
// 添加你的图片(最多6张)
// String[] base64Images = {"图片1base64", "图片2base64", "图片3base64",
// "图片4base64", "图片5base64", "图片6base64"};
String[] base64Images = new String[appOperateResponse.getData().getAppList().size()];
for(int i=0;i<base64Images.length;i++)
{
base64Images[i] = appOperateResponse.getData().getAppList().get(i).getScreenshot();
}
for (int i = 0; i < Math.min(base64Images.length, 6); i++) {
try {
// 为每个应用创建垂直布局
LinearLayout appItem = new LinearLayout(this);
appItem.setOrientation(LinearLayout.VERTICAL);
// 设置应用项布局参数
LinearLayout.LayoutParams appItemParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
appItemParams.setMargins(10, 0, 10, 0);
appItem.setLayoutParams(appItemParams);
// 1. 创建顶部水平布局(引用图标 + 应用名 + 关闭按钮)
LinearLayout topBar = new LinearLayout(this);
topBar.setOrientation(LinearLayout.HORIZONTAL);
topBar.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
topBar.setPadding(5, 5, 5, 5);
// 1.1 引用图标
ImageView iconView = new ImageView(this);
String iconBase64 = iconBase64Images[i];
if (iconBase64.contains(",")) {
iconBase64 = iconBase64.split(",")[1];
}
byte[] iconBytes = Base64.decode(iconBase64, Base64.DEFAULT);
Bitmap iconBitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length);
iconView.setImageBitmap(iconBitmap);
// 设置图标大小
int iconSize = 30; // 30dp
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
dpToPx(iconSize), dpToPx(iconSize)
);
iconParams.setMargins(0, 0, dpToPx(5), 0);
iconView.setLayoutParams(iconParams);
topBar.addView(iconView);
// 1.2 应用名
TextView appNameView = new TextView(this);
appNameView.setText(appNames[i]);
appNameView.setTextColor(0xFFFFFFFF);
appNameView.setTextSize(12);
appNameView.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams nameParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT
);
nameParams.weight = 1; // 占据剩余空间
nameParams.setMargins(0, 0, dpToPx(5), 0);
appNameView.setLayoutParams(nameParams);
topBar.addView(appNameView);
// 1.3 关闭按钮 - 修改高度
Button closeAppBtn = new Button(this);
closeAppBtn.setText("结束应用");
closeAppBtn.setTextSize(10);
closeAppBtn.setTextColor(0xFFFFFFFF);
closeAppBtn.setBackgroundColor(0xFFFF4444);
closeAppBtn.setPadding(dpToPx(5), dpToPx(2), dpToPx(5), dpToPx(2));
// 设置关闭按钮的布局参数,高度设置为原来一半
LinearLayout.LayoutParams closeBtnParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
dpToPx(20) // 高度改为原来的一半(图标是30dp,一半是15dp)
);
closeBtnParams.gravity = Gravity.CENTER_VERTICAL; // 垂直居中
closeAppBtn.setLayoutParams(closeBtnParams);
// 为关闭按钮设置单击事件
int finalI = i;
closeAppBtn.setOnClickListener(v -> {
// 结束应用的逻辑
Toast.makeText(this, "结束应用: " + appNames[finalI], Toast.LENGTH_SHORT).show();
// 这里可以添加实际结束应用的代码
});
topBar.addView(closeAppBtn);
appItem.addView(topBar);
// 2. 创建主图片
String cleanBase64 = base64Images[i];
if (cleanBase64.contains(",")) {
cleanBase64 = cleanBase64.split(",")[1];
}
byte[] bytes = Base64.decode(cleanBase64, Base64.DEFAULT);
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
ImageView iv = new ImageView(this);
iv.setImageBitmap(bitmap);
iv.setScaleType(ImageView.ScaleType.CENTER_CROP);
// 为主图片添加单击事件
int finalI1 = i;
iv.setOnClickListener(v -> {
// 处理主图片单击事件
Toast.makeText(this, "单击了: " + appNames[finalI1], Toast.LENGTH_SHORT).show();
// 这里可以添加单击后的操作,比如跳转到应用详情等
});
// 设置图片布局参数
LinearLayout.LayoutParams imgParams = new LinearLayout.LayoutParams(
imageWidth, imageHeight
);
imgParams.setMargins(0, dpToPx(5), 0, 0);
imgParams.gravity = Gravity.CENTER_HORIZONTAL;
iv.setLayoutParams(imgParams);
appItem.addView(iv);
// 添加到对应的行
if (i < 3) {
row1.addView(appItem);
} else {
row2.addView(appItem);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 添加行布局到主布局
mainLayout.addView(row1);
mainLayout.addView(row2);
dialog.setContentView(mainLayout);
dialog.show();
}
// dp转px的工具方法
private int dpToPx(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density);
}
private void cleanupResources() {
// 停止云手机
if (streamStarted) {
try {
CloudPhoneClient.stop();
} catch (Exception e) {
Log.error(TAG, "停止云手机失败",e);
}
}
// 解绑Fragment
if (m_fragment != null) {
try {
getFragmentManager().beginTransaction()
.remove(m_fragment)
.commitAllowingStateLoss();
m_fragment = null;
Log.info(TAG,"Fragment已移除");
} catch (Exception e) {
Log.error(TAG, "解绑Fragment失败",e);
}
}
// 关闭线程池
if (threadPool != null) {
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(500, TimeUnit.MILLISECONDS)) {
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
threadPool.shutdownNow();
}
}
// 清理RxJava
if (disposables != null && !disposables.isDisposed()) {
disposables.clear();
disposables.dispose();
}
// 清理集合
appButtonList.clear();
appButtonMap.clear();
sysButtonMap.clear();
}
} 父主题: 集成SDK开发Android客户端