,
+ onExampleClick: (example: Example, component: Component) -> Unit,
+ onSettingClick: () -> Unit,
+) {
+ val ltr = LocalLayoutDirection.current
+ APIExampleScaffold(
+ topBarTitle = "Agora API Example",
+ showSettingIcon = true,
+ onSettingClick = onSettingClick,
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier.consumeWindowInsets(paddingValues),
+ contentPadding = PaddingValues(
+ start = paddingValues.calculateStartPadding(ltr) + ComponentPadding,
+ top = paddingValues.calculateTopPadding() + ComponentPadding,
+ end = paddingValues.calculateEndPadding(ltr) + ComponentPadding,
+ bottom = paddingValues.calculateBottomPadding() + ComponentPadding
+ )
+ ) {
+ components.forEach { component ->
+ item {
+ Text(
+ text = component.name,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.height(ComponentPadding))
+ }
+ items(component.examples) { example ->
+ ExampleItem(
+ example = example,
+ onClick = { onExampleClick(example, component) }
+ )
+ Spacer(modifier = Modifier.height(ExampleItemPadding))
+ }
+ }
+
+ }
+ }
+}
+
+private val ComponentPadding = 16.dp
+private val ExampleItemPadding = 8.dp
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/settings/Settings.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/settings/Settings.kt
new file mode 100644
index 000000000..53f31c05f
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/settings/Settings.kt
@@ -0,0 +1,169 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package io.agora.api.example.compose.ui.settings
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.agora.api.example.compose.data.SettingPreferences
+import io.agora.api.example.compose.ui.common.APIExampleScaffold
+import io.agora.api.example.compose.ui.common.DropdownMenuRaw
+import io.agora.rtc2.RtcEngine
+import io.agora.rtc2.RtcEngineConfig
+import io.agora.rtc2.video.VideoEncoderConfiguration
+
+@Composable
+fun Settings(onBackClick: () -> Unit) {
+ APIExampleScaffold(
+ topBarTitle = "Settings",
+ showBackNavigationIcon = true,
+ onBackClick = onBackClick
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .consumeWindowInsets(WindowInsets.safeDrawing)
+ .padding(paddingValues),
+ contentAlignment = Alignment.TopCenter
+ ) {
+
+ Column {
+ val dimensions = listOf(
+ VideoEncoderConfiguration.VD_120x120,
+ VideoEncoderConfiguration.VD_160x120,
+ VideoEncoderConfiguration.VD_180x180,
+ VideoEncoderConfiguration.VD_240x180,
+ VideoEncoderConfiguration.VD_320x180,
+ VideoEncoderConfiguration.VD_240x240,
+ VideoEncoderConfiguration.VD_320x240,
+ VideoEncoderConfiguration.VD_424x240,
+ VideoEncoderConfiguration.VD_360x360,
+ VideoEncoderConfiguration.VD_480x360,
+ VideoEncoderConfiguration.VD_640x360,
+ VideoEncoderConfiguration.VD_640x360,
+ VideoEncoderConfiguration.VD_480x480,
+ VideoEncoderConfiguration.VD_640x480,
+ VideoEncoderConfiguration.VD_840x480,
+ VideoEncoderConfiguration.VD_960x540,
+ VideoEncoderConfiguration.VD_960x720,
+ VideoEncoderConfiguration.VD_1280x720,
+ VideoEncoderConfiguration.VD_1920x1080,
+ VideoEncoderConfiguration.VD_2540x1440,
+ VideoEncoderConfiguration.VD_3840x2160,
+ )
+ DropdownMenuRaw(
+ title = "Dimension",
+ options = dimensions.map {
+ it.toText() to it
+ },
+ selectedValue = SettingPreferences.getVideoDimensions(),
+ ) { _, option ->
+ SettingPreferences.setVideoDimensions(option.second)
+ }
+ Divider(modifier = Modifier.padding(horizontal = 16.dp))
+
+ val frameRates = listOf(
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_1,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_7,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_10,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_24,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30,
+ VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_60,
+ )
+ DropdownMenuRaw(
+ title = "FrameRate",
+ options = frameRates.map { it.toText() to it},
+ selectedValue = SettingPreferences.getVideoFrameRate(),
+ ) { _, option ->
+ SettingPreferences.setVideoFrameRate(option.second)
+ }
+ Divider(modifier = Modifier.padding(horizontal = 16.dp))
+
+ val orientationMode = listOf(
+ VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE,
+ VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_LANDSCAPE,
+ VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT,
+ )
+ DropdownMenuRaw(
+ title = "Orientation",
+ options = orientationMode.map { it.toText() to it},
+ selectedValue = SettingPreferences.getOrientationMode(),
+ ) { _, option ->
+ SettingPreferences.setOrientationMode(option.second)
+ }
+
+ Divider(modifier = Modifier.padding(horizontal = 16.dp))
+
+ DropdownMenuRaw(
+ title = "Area",
+ options = listOf(
+ "Glob" to RtcEngineConfig.AreaCode.AREA_CODE_GLOB,
+ "China" to RtcEngineConfig.AreaCode.AREA_CODE_CN,
+ "North America" to RtcEngineConfig.AreaCode.AREA_CODE_NA,
+ "Europe" to RtcEngineConfig.AreaCode.AREA_CODE_EU,
+ "Asia Pacific" to RtcEngineConfig.AreaCode.AREA_CODE_AS,
+ "JP" to RtcEngineConfig.AreaCode.AREA_CODE_JP,
+ "IN" to RtcEngineConfig.AreaCode.AREA_CODE_IN,
+ ),
+ selectedValue = SettingPreferences.getArea(),
+ ) { _, option ->
+ SettingPreferences.setArea(option.second)
+ }
+
+ Divider(modifier = Modifier.padding(horizontal = 16.dp))
+
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = "SDK Version: ${RtcEngine.getSdkVersion()}",
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ textAlign = TextAlign.End
+ )
+ }
+
+ }
+ }
+
+}
+
+
+
+@Preview
+@Composable
+fun SettingsPreview(){
+ Settings {
+
+ }
+}
+
+fun VideoEncoderConfiguration.VideoDimensions.toText(): String{
+ return "VD_${this.width}x${this.height}"
+}
+
+fun VideoEncoderConfiguration.FRAME_RATE.toText() : String{
+ return this.name.substring(11)
+}
+
+fun VideoEncoderConfiguration.ORIENTATION_MODE.toText() : String{
+ return this.name.substring(17)
+}
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/theme/Theme.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/theme/Theme.kt
new file mode 100644
index 000000000..8d7814829
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/ui/theme/Theme.kt
@@ -0,0 +1,39 @@
+package io.agora.api.example.compose.ui.theme
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+
+@Composable
+fun APIExampleComposeTheme(
+ content: @Composable () -> Unit
+) {
+ val darkTheme = false
+ val colorScheme = if (!darkTheme) lightColorScheme() else darkColorScheme()
+ val view = LocalView.current
+ val context = LocalContext.current
+ SideEffect {
+ WindowCompat.getInsetsController(context.findActivity().window, view)
+ .isAppearanceLightStatusBars = !darkTheme
+ }
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content
+ )
+}
+
+private tailrec fun Context.findActivity(): Activity =
+ when (this) {
+ is Activity -> this
+ is ContextWrapper -> this.baseContext.findActivity()
+ else -> throw IllegalArgumentException("Could not find activity!")
+ }
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioFileReader.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioFileReader.java
new file mode 100644
index 000000000..5eb43552e
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioFileReader.java
@@ -0,0 +1,158 @@
+package io.agora.api.example.compose.utils;
+
+import android.content.Context;
+import android.os.Process;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * The type Audio file reader.
+ */
+public class AudioFileReader {
+ private static final String AUDIO_FILE = "output.raw";
+ /**
+ * The constant SAMPLE_RATE.
+ */
+ public static final int SAMPLE_RATE = 44100;
+ /**
+ * The constant SAMPLE_NUM_OF_CHANNEL.
+ */
+ public static final int SAMPLE_NUM_OF_CHANNEL = 2;
+ /**
+ * The constant BITS_PER_SAMPLE.
+ */
+ public static final int BITS_PER_SAMPLE = 16;
+
+ /**
+ * The constant BYTE_PER_SAMPLE.
+ */
+ public static final float BYTE_PER_SAMPLE = 1.0f * BITS_PER_SAMPLE / 8 * SAMPLE_NUM_OF_CHANNEL;
+ /**
+ * The constant DURATION_PER_SAMPLE.
+ */
+ public static final float DURATION_PER_SAMPLE = 1000.0f / SAMPLE_RATE; // ms
+ /**
+ * The constant SAMPLE_COUNT_PER_MS.
+ */
+ public static final float SAMPLE_COUNT_PER_MS = SAMPLE_RATE * 1.0f / 1000; // ms
+
+ private static final int BUFFER_SAMPLE_COUNT = (int) (SAMPLE_COUNT_PER_MS * 10); // 10ms sample count
+ private static final int BUFFER_BYTE_SIZE = (int) (BUFFER_SAMPLE_COUNT * BYTE_PER_SAMPLE); // byte
+ private static final long BUFFER_DURATION = (long) (BUFFER_SAMPLE_COUNT * DURATION_PER_SAMPLE); // ms
+
+ private final Context context;
+ private final OnAudioReadListener audioReadListener;
+ private volatile boolean pushing = false;
+ private InnerThread thread;
+ private InputStream inputStream;
+
+ /**
+ * Instantiates a new Audio file reader.
+ *
+ * @param context the context
+ * @param listener the listener
+ */
+ public AudioFileReader(Context context, OnAudioReadListener listener) {
+ this.context = context;
+ this.audioReadListener = listener;
+ }
+
+ /**
+ * Start.
+ */
+ public void start() {
+ if (thread == null) {
+ thread = new InnerThread();
+ thread.start();
+ }
+ }
+
+ /**
+ * Stop.
+ */
+ public void stop() {
+ pushing = false;
+ if (thread != null) {
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ thread = null;
+ }
+ }
+ }
+
+ /**
+ * The interface On audio read listener.
+ */
+ public interface OnAudioReadListener {
+ /**
+ * On audio read.
+ *
+ * @param buffer the buffer
+ * @param timestamp the timestamp
+ */
+ void onAudioRead(byte[] buffer, long timestamp);
+ }
+
+ private final class InnerThread extends Thread {
+
+ @Override
+ public void run() {
+ super.run();
+ try {
+ inputStream = context.getAssets().open(AUDIO_FILE);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
+ pushing = true;
+
+ long start_time = System.currentTimeMillis();
+ int sent_audio_frames = 0;
+ while (pushing) {
+ if (audioReadListener != null) {
+ audioReadListener.onAudioRead(readBuffer(), System.currentTimeMillis());
+ }
+ ++sent_audio_frames;
+ long next_frame_start_time = sent_audio_frames * BUFFER_DURATION + start_time;
+ long now = System.currentTimeMillis();
+
+ if (next_frame_start_time > now) {
+ long sleep_duration = next_frame_start_time - now;
+ try {
+ Thread.sleep(sleep_duration);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ inputStream = null;
+ }
+ }
+ }
+
+ private byte[] readBuffer() {
+ int byteSize = BUFFER_BYTE_SIZE;
+ byte[] buffer = new byte[byteSize];
+ try {
+ if (inputStream.read(buffer) < 0) {
+ inputStream.reset();
+ return readBuffer();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return buffer;
+ }
+ }
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioPlayer.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioPlayer.java
new file mode 100644
index 000000000..a46c94b09
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AudioPlayer.java
@@ -0,0 +1,115 @@
+package io.agora.api.example.compose.utils;
+
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * The type Audio player.
+ */
+public class AudioPlayer {
+
+ private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;
+ private static final String TAG = "AudioPlayer";
+
+ private AudioTrack mAudioTrack;
+ private AudioStatus mAudioStatus = AudioStatus.STOPPED;
+
+ private final int streamType;
+ private final int format;
+ private final int sampleRateInHz;
+ private final int audioFormat;
+ private final int mMinBufferSize;
+
+ /**
+ * Instantiates a new Audio player.
+ *
+ * @param streamType the stream type
+ * @param sampleRateInHz the sample rate in hz
+ * @param channelConfig the channel config
+ * @param audioFormat the audio format
+ */
+ public AudioPlayer(int streamType, int sampleRateInHz, int channelConfig, int audioFormat) {
+ this.streamType = streamType;
+ this.sampleRateInHz = sampleRateInHz;
+ this.audioFormat = audioFormat;
+
+ if (1 == channelConfig) {
+ format = AudioFormat.CHANNEL_OUT_MONO;
+ } else if (2 == channelConfig) {
+ format = AudioFormat.CHANNEL_OUT_STEREO;
+ } else {
+ format = AudioFormat.CHANNEL_OUT_MONO;
+ }
+
+ mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, format, audioFormat);
+ Log.e(TAG, " sampleRateInHz :" + sampleRateInHz + " channelConfig :" + channelConfig + " audioFormat: " + audioFormat + " mMinBufferSize: " + mMinBufferSize);
+ if (mMinBufferSize == AudioTrack.ERROR_BAD_VALUE) {
+ Log.e(TAG, "AudioTrack.ERROR_BAD_VALUE : " + AudioTrack.ERROR_BAD_VALUE);
+ }
+ }
+
+ /**
+ * Start player boolean.
+ *
+ * @return the boolean
+ */
+ public boolean startPlayer() {
+ if (mAudioStatus == AudioStatus.STOPPED) {
+
+ mAudioTrack = new AudioTrack(streamType, sampleRateInHz, format, audioFormat, mMinBufferSize, DEFAULT_PLAY_MODE);
+ if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
+ throw new RuntimeException("Error on AudioTrack created");
+ }
+
+ mAudioTrack.play();
+ mAudioStatus = AudioStatus.RUNNING;
+ }
+ Log.e("AudioPlayer", "mAudioStatus: " + mAudioStatus);
+ return true;
+ }
+
+ /**
+ * Stop player.
+ */
+ public void stopPlayer() {
+ if (null != mAudioTrack) {
+ mAudioStatus = AudioStatus.STOPPED;
+ mAudioTrack.stop();
+ mAudioTrack.release();
+ mAudioTrack = null;
+ }
+ Log.e(TAG, "mAudioStatus: " + mAudioStatus);
+ }
+
+ /**
+ * Play boolean.
+ *
+ * @param audioData the audio data
+ * @param offsetInBytes the offset in bytes
+ * @param sizeInBytes the size in bytes
+ * @return the boolean
+ */
+ public boolean play(byte[] audioData, int offsetInBytes, int sizeInBytes) {
+ if (mAudioStatus == AudioStatus.RUNNING) {
+ mAudioTrack.write(audioData, offsetInBytes, sizeInBytes);
+ } else {
+ Log.e(TAG, "=== No data to AudioTrack !! mAudioStatus: " + mAudioStatus);
+ }
+ return true;
+ }
+
+ /**
+ * The enum Audio status.
+ */
+ public enum AudioStatus {
+ /**
+ * Running audio status.
+ */
+ RUNNING,
+ /**
+ * Stopped audio status.
+ */
+ STOPPED
+ }
+}
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/FileUtils.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/FileUtils.java
new file mode 100644
index 000000000..82bf81527
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/FileUtils.java
@@ -0,0 +1,161 @@
+package io.agora.api.example.compose.utils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.text.TextUtils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * The type File utils.
+ */
+public final class FileUtils {
+
+ private FileUtils() {
+
+ }
+
+ /**
+ * The constant SEPARATOR.
+ */
+ public static final String SEPARATOR = File.separator;
+
+ /**
+ * Copy files from assets.
+ *
+ * @param context the context
+ * @param assetsPath the assets path
+ * @param storagePath the storage path
+ */
+ public static void copyFilesFromAssets(Context context, String assetsPath, String storagePath) {
+ String temp = "";
+
+ if (TextUtils.isEmpty(storagePath)) {
+ return;
+ } else if (storagePath.endsWith(SEPARATOR)) {
+ storagePath = storagePath.substring(0, storagePath.length() - 1);
+ }
+
+ if (TextUtils.isEmpty(assetsPath) || assetsPath.equals(SEPARATOR)) {
+ assetsPath = "";
+ } else if (assetsPath.endsWith(SEPARATOR)) {
+ assetsPath = assetsPath.substring(0, assetsPath.length() - 1);
+ }
+
+ AssetManager assetManager = context.getAssets();
+ try {
+ File file = new File(storagePath);
+ if (!file.exists()) { //如果文件夹不存在,则创建新的文件夹
+ file.mkdirs();
+ }
+
+ // 获取assets目录下的所有文件及目录名
+ String[] fileNames = assetManager.list(assetsPath);
+ if (fileNames.length > 0) { //如果是目录 apk
+ for (String fileName : fileNames) {
+ if (!TextUtils.isEmpty(assetsPath)) {
+ temp = assetsPath + SEPARATOR + fileName; //补全assets资源路径
+ }
+
+ String[] childFileNames = assetManager.list(temp);
+ if (!TextUtils.isEmpty(temp) && childFileNames.length > 0) { //判断是文件还是文件夹:如果是文件夹
+ copyFilesFromAssets(context, temp, storagePath + SEPARATOR + fileName);
+ } else { //如果是文件
+ InputStream inputStream = assetManager.open(temp);
+ readInputStream(storagePath + SEPARATOR + fileName, inputStream);
+ }
+ }
+ } else { //如果是文件 doc_test.txt或者apk/app_test.apk
+ InputStream inputStream = assetManager.open(assetsPath);
+ if (assetsPath.contains(SEPARATOR)) { //apk/app_test.apk
+ assetsPath = assetsPath.substring(assetsPath.lastIndexOf(SEPARATOR), assetsPath.length());
+ }
+ readInputStream(storagePath + SEPARATOR + assetsPath, inputStream);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ /**
+ * 读取输入流中的数据写入输出流
+ *
+ * @param storagePath 目标文件路径
+ * @param inputStream 输入流
+ */
+ public static void readInputStream(String storagePath, InputStream inputStream) {
+ File file = new File(storagePath);
+ try {
+ if (!file.exists()) {
+ // 1.建立通道对象
+ FileOutputStream fos = new FileOutputStream(file);
+ // 2.定义存储空间
+ byte[] buffer = new byte[inputStream.available()];
+ // 3.开始读文件
+ int lenght = 0;
+ while ((lenght = inputStream.read(buffer)) != -1) { // 循环从输入流读取buffer字节
+ // 将Buffer中的数据写到outputStream对象中
+ fos.write(buffer, 0, lenght);
+ }
+ fos.flush(); // 刷新缓冲区
+ // 4.关闭流
+ fos.close();
+ inputStream.close();
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @NotNull
+ public static String getAssetsString(@NotNull Context context, @NotNull String path) {
+ StringBuilder sb = new StringBuilder();
+ InputStreamReader isr = null;
+ BufferedReader br = null;
+
+ try {
+ isr = new InputStreamReader(context.getResources().getAssets().open(path));
+ br = new BufferedReader(isr);
+ String line = null;
+ while ((line = br.readLine()) != null){
+ sb.append(line).append("\n");
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (isr != null) {
+ try {
+ isr.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (br != null) {
+ try {
+ br.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return sb.toString();
+ }
+}
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/GLTextureView.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/GLTextureView.java
new file mode 100644
index 000000000..9397f2bf3
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/GLTextureView.java
@@ -0,0 +1,1984 @@
+package io.agora.api.example.compose.utils;
+
+import static android.opengl.EGL14.EGL_BAD_ACCESS;
+import static android.opengl.EGL14.EGL_BAD_ALLOC;
+import static android.opengl.EGL14.EGL_BAD_ATTRIBUTE;
+import static android.opengl.EGL14.EGL_BAD_CONFIG;
+import static android.opengl.EGL14.EGL_BAD_CONTEXT;
+import static android.opengl.EGL14.EGL_BAD_CURRENT_SURFACE;
+import static android.opengl.EGL14.EGL_BAD_DISPLAY;
+import static android.opengl.EGL14.EGL_BAD_MATCH;
+import static android.opengl.EGL14.EGL_BAD_NATIVE_PIXMAP;
+import static android.opengl.EGL14.EGL_BAD_NATIVE_WINDOW;
+import static android.opengl.EGL14.EGL_BAD_PARAMETER;
+import static android.opengl.EGL14.EGL_BAD_SURFACE;
+import static android.opengl.EGL14.EGL_NOT_INITIALIZED;
+import static android.opengl.EGL14.EGL_SUCCESS;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.opengl.EGL14;
+import android.opengl.EGLExt;
+import android.opengl.GLDebugHelper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.View;
+
+import java.io.Writer;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGL11;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * 参考 {@link GLSurfaceView} 实现
+ *
+ * @author fkwl5
+ */
+public class GLTextureView extends TextureView implements TextureView.SurfaceTextureListener, View.OnLayoutChangeListener {
+ private final static String TAG = "GLTextureView";
+ private final static boolean LOG_ATTACH_DETACH = true;
+ private final static boolean LOG_THREADS = false;
+ private final static boolean LOG_PAUSE_RESUME = true;
+ private final static boolean LOG_SURFACE = true;
+ private final static boolean LOG_RENDERER = true;
+ private final static boolean LOG_RENDERER_DRAW_FRAME = false;
+ private final static boolean LOG_EGL = true;
+ /**
+ * The renderer only renders
+ * when the surface is created, or when {@link #requestRender} is called.
+ *
+ * @see #getRenderMode() #getRenderMode()
+ * @see #setRenderMode(int) #setRenderMode(int)
+ * @see #requestRender() #requestRender()
+ */
+ public final static int RENDERMODE_WHEN_DIRTY = 0;
+ /**
+ * The renderer is called
+ * continuously to re-render the scene.
+ *
+ * @see #getRenderMode() #getRenderMode()
+ * @see #setRenderMode(int) #setRenderMode(int)
+ */
+ public final static int RENDERMODE_CONTINUOUSLY = 1;
+
+ /**
+ * Check glError() after every GL call and throw an exception if glError indicates
+ * that an error has occurred. This can be used to help track down which OpenGL ES call
+ * is causing an error.
+ *
+ * @see #getDebugFlags #getDebugFlags
+ * @see #setDebugFlags #setDebugFlags
+ */
+ public final static int DEBUG_CHECK_GL_ERROR = 1;
+
+ /**
+ * Log GL calls to the system log at "verbose" level with tag "GLTextureView".
+ *
+ * @see #getDebugFlags #getDebugFlags
+ * @see #setDebugFlags #setDebugFlags
+ */
+ public final static int DEBUG_LOG_GL_CALLS = 2;
+
+ /**
+ * 构造方法,必须调用 {@link #setRenderer} 才能进行渲染
+ *
+ * @param context the context
+ */
+ public GLTextureView(Context context) {
+ super(context);
+ init();
+ }
+
+ /**
+ * Instantiates a new Gl texture view.
+ *
+ * @param context the context
+ * @param attributeSet the attribute set
+ */
+ public GLTextureView(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ init();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mGLThread != null) {
+ // GLThread may still be running if this view was never
+ // attached to a window.
+ mGLThread.requestExitAndWait();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void init() {
+ setSurfaceTextureListener(this);
+ }
+
+ /**
+ * Set the glWrapper. If the glWrapper is not null, its
+ * {@link GLWrapper#wrap(GL)} method is called
+ * whenever a surface is created. A GLWrapper can be used to wrap
+ * the GL object that's passed to the renderer. Wrapping a GL
+ * object enables examining and modifying the behavior of the
+ * GL calls made by the renderer.
+ *
+ * Wrapping is typically used for debugging purposes.
+ *
+ * The default value is null.
+ *
+ * @param glWrapper the new GLWrapper
+ */
+ public void setGLWrapper(GLWrapper glWrapper) {
+ mGLWrapper = glWrapper;
+ }
+
+ /**
+ * Set the debug flags to a new value. The value is
+ * constructed by OR-together zero or more
+ * of the DEBUG_CHECK_* constants. The debug flags take effect
+ * whenever a surface is created. The default value is zero.
+ *
+ * @param debugFlags the new debug flags
+ * @see #DEBUG_CHECK_GL_ERROR #DEBUG_CHECK_GL_ERROR
+ * @see #DEBUG_LOG_GL_CALLS #DEBUG_LOG_GL_CALLS
+ */
+ public void setDebugFlags(int debugFlags) {
+ mDebugFlags = debugFlags;
+ }
+
+ /**
+ * Get the current value of the debug flags.
+ *
+ * @return the current value of the debug flags.
+ */
+ public int getDebugFlags() {
+ return mDebugFlags;
+ }
+
+ /**
+ * Control whether the EGL context is preserved when the GLTextureView is paused and
+ * resumed.
+ *
+ * If set to true, then the EGL context may be preserved when the GLTextureView is paused.
+ *
+ * Prior to API level 11, whether the EGL context is actually preserved or not
+ * depends upon whether the Android device can support an arbitrary number of
+ * EGL contexts or not. Devices that can only support a limited number of EGL
+ * contexts must release the EGL context in order to allow multiple applications
+ * to share the GPU.
+ *
+ * If set to false, the EGL context will be released when the GLTextureView is paused,
+ * and recreated when the GLTextureView is resumed.
+ *
+ *
+ * The default is false.
+ *
+ * @param preserveOnPause preserve the EGL context when paused
+ */
+ public void setPreserveEGLContextOnPause(boolean preserveOnPause) {
+ mPreserveEGLContextOnPause = preserveOnPause;
+ }
+
+ /**
+ * Gets preserve egl context on pause.
+ *
+ * @return true if the EGL context will be preserved when paused
+ */
+ public boolean getPreserveEGLContextOnPause() {
+ return mPreserveEGLContextOnPause;
+ }
+
+ /**
+ * Set the renderer associated with this view. Also starts the thread that
+ * will call the renderer, which in turn causes the rendering to start.
+ *
This method should be called once and only once in the life-cycle of
+ * a GLTextureView.
+ *
The following GLTextureView methods can only be called before
+ * setRenderer is called:
+ *
+ * - {@link #setEGLConfigChooser(boolean)}
+ *
- {@link #setEGLConfigChooser(EGLConfigChooser)}
+ *
- {@link #setEGLConfigChooser(int, int, int, int, int, int)}
+ *
+ *
+ * The following GLTextureView methods can only be called after
+ * setRenderer is called:
+ *
+ * - {@link #getRenderMode()}
+ *
- {@link #onPause()}
+ *
- {@link #onResume()}
+ *
- {@link #queueEvent(Runnable)}
+ *
- {@link #requestRender()}
+ *
- {@link #setRenderMode(int)}
+ *
+ *
+ * @param renderer the renderer to use to perform OpenGL drawing.
+ */
+ public void setRenderer(Renderer renderer) {
+ checkRenderThreadState();
+ if (mEGLConfigChooser == null) {
+ mEGLConfigChooser = new SimpleEGLConfigChooser(true);
+ }
+ if (mEGLContextFactory == null) {
+ mEGLContextFactory = new DefaultContextFactory();
+ }
+ if (mEGLWindowSurfaceFactory == null) {
+ mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
+ }
+ mRenderer = renderer;
+ mGLThread = new GLThread(mThisWeakRef);
+ mGLThread.start();
+ }
+
+ /**
+ * Install a custom EGLContextFactory.
+ * If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If this method is not called, then by default
+ * a context will be created with no shared context and
+ * with a null attribute list.
+ *
+ * @param factory the factory
+ */
+ public void setEGLContextFactory(EGLContextFactory factory) {
+ checkRenderThreadState();
+ mEGLContextFactory = factory;
+ }
+
+ /**
+ * Install a custom EGLWindowSurfaceFactory.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If this method is not called, then by default
+ * a window surface will be created with a null attribute list.
+ *
+ * @param factory the factory
+ */
+ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) {
+ checkRenderThreadState();
+ mEGLWindowSurfaceFactory = factory;
+ }
+
+ /**
+ * Install a custom EGLConfigChooser.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If no setEGLConfigChooser method is called, then by default the
+ * view will choose an EGLConfig that is compatible with the current
+ * android.view.Surface, with a depth buffer depth of
+ * at least 16 bits.
+ *
+ * @param configChooser the config chooser
+ */
+ public void setEGLConfigChooser(EGLConfigChooser configChooser) {
+ checkRenderThreadState();
+ mEGLConfigChooser = configChooser;
+ }
+
+ /**
+ * Install a config chooser which will choose a config
+ * as close to 16-bit RGB as possible, with or without an optional depth
+ * buffer as close to 16-bits as possible.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If no setEGLConfigChooser method is called, then by default the
+ * view will choose an RGB_888 surface with a depth buffer depth of
+ * at least 16 bits.
+ *
+ * @param needDepth the need depth
+ */
+ public void setEGLConfigChooser(boolean needDepth) {
+ setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth));
+ }
+
+ /**
+ * Install a config chooser which will choose a config
+ * with at least the specified depthSize and stencilSize,
+ * and exactly the specified redSize, greenSize, blueSize and alphaSize.
+ *
If this method is
+ * called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
+ * If no setEGLConfigChooser method is called, then by default the
+ * view will choose an RGB_888 surface with a depth buffer depth of
+ * at least 16 bits.
+ *
+ * @param redSize the red size
+ * @param greenSize the green size
+ * @param blueSize the blue size
+ * @param alphaSize the alpha size
+ * @param depthSize the depth size
+ * @param stencilSize the stencil size
+ */
+ public void setEGLConfigChooser(int redSize, int greenSize, int blueSize,
+ int alphaSize, int depthSize, int stencilSize) {
+ setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize,
+ blueSize, alphaSize, depthSize, stencilSize));
+ }
+
+ /**
+ * Inform the default EGLContextFactory and default EGLConfigChooser
+ * which EGLContext client version to pick.
+ *
Use this method to create an OpenGL ES 2.0-compatible context.
+ * Example:
+ *
+ * public MyView(Context context) {
+ * super(context);
+ * setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+ * setRenderer(new MyRenderer());
+ * }
+ *
+ * Note: Activities which require OpenGL ES 2.0 should indicate this by
+ * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's
+ * AndroidManifest.xml file.
+ *
If this method is called, it must be called before {@link #setRenderer(Renderer)}
+ * is called.
+ *
This method only affects the behavior of the default EGLContexFactory and the
+ * default EGLConfigChooser. If
+ * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied
+ * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context.
+ * If
+ * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied
+ * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config.
+ *
+ * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0
+ */
+ public void setEGLContextClientVersion(int version) {
+ checkRenderThreadState();
+ mEGLContextClientVersion = version;
+ }
+
+ /**
+ * Set the rendering mode. When renderMode is
+ * RENDERMODE_CONTINUOUSLY, the renderer is called
+ * repeatedly to re-render the scene. When renderMode
+ * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface
+ * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY.
+ *
+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance
+ * by allowing the GPU and CPU to idle when the view does not need to be updated.
+ *
+ * This method can only be called after {@link #setRenderer(Renderer)}
+ *
+ * @param renderMode one of the RENDERMODE_X constants
+ * @see #RENDERMODE_CONTINUOUSLY #RENDERMODE_CONTINUOUSLY
+ * @see #RENDERMODE_WHEN_DIRTY #RENDERMODE_WHEN_DIRTY
+ */
+ public void setRenderMode(int renderMode) {
+ mGLThread.setRenderMode(renderMode);
+ }
+
+ /**
+ * Get the current rendering mode. May be called
+ * from any thread. Must not be called before a renderer has been set.
+ *
+ * @return the current rendering mode.
+ * @see #RENDERMODE_CONTINUOUSLY #RENDERMODE_CONTINUOUSLY
+ * @see #RENDERMODE_WHEN_DIRTY #RENDERMODE_WHEN_DIRTY
+ */
+ public int getRenderMode() {
+ return mGLThread.getRenderMode();
+ }
+
+ /**
+ * Request that the renderer render a frame.
+ * This method is typically used when the render mode has been set to
+ * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand.
+ * May be called
+ * from any thread. Must not be called before a renderer has been set.
+ */
+ public void requestRender() {
+ mGLThread.requestRender();
+ }
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ mGLThread.surfaceCreated();
+ mGLThread.onWindowResize(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ mGLThread.onWindowResize(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ mGLThread.surfaceDestroyed();
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ mGLThread.onWindowResize(right - left, bottom - top);
+ }
+
+ /**
+ * Pause the rendering thread, optionally tearing down the EGL context
+ * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}.
+ *
+ * Must not be called before a renderer has been set.
+ */
+ public void onPause() {
+ mGLThread.onPause();
+ }
+
+ /**
+ * Resumes the rendering thread, re-creating the OpenGL context if necessary. It
+ * is the counterpart to {@link #onPause()}.
+ *
+ * Must not be called before a renderer has been set.
+ */
+ public void onResume() {
+ mGLThread.onResume();
+ }
+
+ /**
+ * Queue a runnable to be run on the GL rendering thread. This can be used
+ * to communicate with the Renderer on the rendering thread.
+ * Must not be called before a renderer has been set.
+ *
+ * @param r the runnable to be run on the GL rendering thread.
+ */
+ public void queueEvent(Runnable r) {
+ mGLThread.queueEvent(r);
+ }
+
+ /**
+ * This method is used as part of the View class and is not normally
+ * called or subclassed by clients of GLTextureView.
+ */
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (LOG_ATTACH_DETACH) {
+ Log.d(TAG, "onAttachedToWindow reattach =" + mDetached);
+ }
+ if (mDetached && mRenderer != null) {
+ int renderMode = RENDERMODE_CONTINUOUSLY;
+ if (mGLThread != null) {
+ renderMode = mGLThread.getRenderMode();
+ }
+ mGLThread = new GLThread(mThisWeakRef);
+ if (renderMode != RENDERMODE_CONTINUOUSLY) {
+ mGLThread.setRenderMode(renderMode);
+ }
+ mGLThread.start();
+ }
+ mDetached = false;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (LOG_ATTACH_DETACH) {
+ Log.d(TAG, "onDetachedFromWindow");
+ }
+ if (mGLThread != null) {
+ mGLThread.requestExitAndWait();
+ }
+ mDetached = true;
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * Gets egl context.
+ *
+ * @return the egl context
+ */
+ public EGLContext getEglContext() {
+ return mGLThread.getEglContext();
+ }
+
+ // --------------------------------------------------------------
+
+ /**
+ * An interface used to wrap a GL interface.
+ * Wrapping is typically used for debugging purposes.
+ */
+ public interface GLWrapper {
+ /**
+ * Wraps a gl interface in another gl interface.
+ *
+ * @param gl a GL interface that is to be wrapped.
+ * @return either the input argument or another GL object that wraps the input argument.
+ */
+ GL wrap(GL gl);
+ }
+
+ /**
+ * A generic renderer interface.
+ *
+ * The renderer is responsible for making OpenGL calls to render a frame.
+ *
+ * GLTextureView clients typically create their own classes that implement
+ * this interface, and then call {@link GLTextureView#setRenderer} to
+ * register the renderer with the GLTextureView.
+ *
+ *
+ *
Threading
+ * The renderer will be called on a separate thread, so that rendering
+ * performance is decoupled from the UI thread. Clients typically need to
+ * communicate with the renderer from the UI thread, because that's where
+ * input events are received. Clients can communicate using any of the
+ * standard Java techniques for cross-thread communication, or they can
+ * use the {@link GLTextureView#queueEvent(Runnable)} convenience method.
+ *
+ *
EGL Context Lost
+ * There are situations where the EGL rendering context will be lost. This
+ * typically happens when device wakes up after going to sleep. When
+ * the EGL context is lost, all OpenGL resources (such as textures) that are
+ * associated with that context will be automatically deleted. In order to
+ * keep rendering correctly, a renderer must recreate any lost resources
+ * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method
+ * is a convenient place to do this.
+ *
+ * @see GLTextureView#setRenderer(Renderer) GLTextureView#setRenderer(GLTextureView.Renderer)
+ */
+ public interface Renderer {
+ /**
+ * Called when the surface is created or recreated.
+ *
+ * @param gl the GL interface. Use instanceof to test if the interface supports GL11 or higher interfaces.
+ * @param config the EGLConfig of the created surface. Can be used to create matching pbuffers.
+ */
+ void onSurfaceCreated(GL10 gl, EGLConfig config);
+
+ /**
+ * Called when the surface changed size.
+ *
+ * Called after the surface is created and whenever
+ * the OpenGL ES surface size changes.
+ *
+ *
+ * @param gl the GL interface. Use instanceof to test if the interface supports GL11 or higher interfaces.
+ * @param width the width
+ * @param height the height
+ */
+ void onSurfaceChanged(GL10 gl, int width, int height);
+
+ /**
+ * Called to draw the current frame.
+ *
+ * This method is responsible for drawing the current frame.
+ *
+ *
+ * @param gl the GL interface. Use instanceof to test if the interface supports GL11 or higher interfaces.
+ */
+ void onDrawFrame(GL10 gl);
+ }
+
+ /**
+ * An interface for customizing the eglCreateContext and eglDestroyContext calls.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLTextureView#setEGLContextFactory(EGLContextFactory)}
+ */
+ public interface EGLContextFactory {
+ /**
+ * Create context egl context.
+ *
+ * @param egl the egl
+ * @param display the display
+ * @param eglConfig the egl config
+ * @return the egl context
+ */
+ EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig);
+
+ /**
+ * Destroy context.
+ *
+ * @param egl the egl
+ * @param display the display
+ * @param context the context
+ */
+ void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context);
+ }
+
+ private final class DefaultContextFactory implements EGLContextFactory {
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+
+ @Override
+ public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {
+ int[] attributeList = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion,
+ EGL10.EGL_NONE};
+
+ return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT,
+ mEGLContextClientVersion != 0 ? attributeList : null);
+ }
+
+ @Override
+ public void destroyContext(EGL10 egl, EGLDisplay display,
+ EGLContext context) {
+ if (!egl.eglDestroyContext(display, context)) {
+ Log.e("DefaultContextFactory", "display:" + display + " context: " + context);
+ if (LOG_THREADS) {
+ Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId());
+ }
+ EglHelper.throwEglException("eglDestroyContext", egl.eglGetError());
+ }
+ }
+ }
+
+ /**
+ * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLTextureView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)}
+ */
+ public interface EGLWindowSurfaceFactory {
+ /**
+ * Create window surface egl surface.
+ *
+ * @param egl the egl
+ * @param display the display
+ * @param config the config
+ * @param nativeWindow the native window
+ * @return null if the surface cannot be constructed.
+ */
+ EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config,
+ Object nativeWindow);
+
+ /**
+ * Destroy surface.
+ *
+ * @param egl the egl
+ * @param display the display
+ * @param surface the surface
+ */
+ void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface);
+ }
+
+ private static final class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory {
+
+ @Override
+ public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display,
+ EGLConfig config, Object nativeWindow) {
+ EGLSurface result = null;
+ try {
+ result = egl.eglCreateWindowSurface(display, config, nativeWindow, null);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "eglCreateWindowSurface", e);
+ }
+ return result;
+ }
+
+ @Override
+ public void destroySurface(EGL10 egl, EGLDisplay display,
+ EGLSurface surface) {
+ egl.eglDestroySurface(display, surface);
+ }
+ }
+
+ /**
+ * An interface for choosing an EGLConfig configuration from a list of
+ * potential configurations.
+ *
+ * This interface must be implemented by clients wishing to call
+ * {@link GLTextureView#setEGLConfigChooser(EGLConfigChooser)}
+ */
+ public interface EGLConfigChooser {
+ /**
+ * Choose a configuration from the list. Implementors typically
+ * implement this method by calling
+ * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the
+ * EGL specification available from The Khronos Group to learn how to call eglChooseConfig.
+ *
+ * @param egl the EGL10 for the current display.
+ * @param display the current display.
+ * @return the chosen configuration.
+ */
+ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
+ }
+
+ private abstract class BaseConfigChooser implements EGLConfigChooser {
+ /**
+ * Instantiates a new Base config chooser.
+ *
+ * @param configSpec the config spec
+ */
+ private BaseConfigChooser(int[] configSpec) {
+ mConfigSpec = filterConfigSpec(configSpec);
+ }
+
+ @Override
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ int[] num_config = new int[1];
+ if (!egl.eglChooseConfig(display, mConfigSpec, null, 0,
+ num_config)) {
+ throw new IllegalArgumentException("eglChooseConfig failed");
+ }
+
+ int numConfigs = num_config[0];
+
+ if (numConfigs <= 0) {
+ throw new IllegalArgumentException(
+ "No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs];
+ if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs,
+ num_config)) {
+ throw new IllegalArgumentException("eglChooseConfig#2 failed");
+ }
+ EGLConfig config = chooseConfig(egl, display, configs);
+ if (config == null) {
+ throw new IllegalArgumentException("No config chosen");
+ }
+ return config;
+ }
+
+ /**
+ * Choose config egl config.
+ *
+ * @param egl the egl
+ * @param display the display
+ * @param configs the configs
+ * @return the egl config
+ */
+ abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
+ EGLConfig[] configs);
+
+ /**
+ * The M config spec.
+ */
+ protected int[] mConfigSpec;
+
+ private int[] filterConfigSpec(int[] configSpec) {
+ if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) {
+ return configSpec;
+ }
+ /* We know none of the subclasses define EGL_RENDERABLE_TYPE.
+ * And we know the configSpec is well formed.
+ */
+ int len = configSpec.length;
+ int[] newConfigSpec = new int[len + 2];
+ System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1);
+ newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE;
+ if (mEGLContextClientVersion == 2) {
+ newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */
+ } else {
+ newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */
+ }
+ newConfigSpec[len + 1] = EGL10.EGL_NONE;
+ return newConfigSpec;
+ }
+ }
+
+ /**
+ * Choose a configuration with exactly the specified r,g,b,a sizes,
+ * and at least the specified depth and stencil sizes.
+ */
+ private class ComponentSizeChooser extends BaseConfigChooser {
+ private ComponentSizeChooser(int redSize, int greenSize, int blueSize,
+ int alphaSize, int depthSize, int stencilSize) {
+ super(new int[]{
+ EGL10.EGL_RED_SIZE, redSize,
+ EGL10.EGL_GREEN_SIZE, greenSize,
+ EGL10.EGL_BLUE_SIZE, blueSize,
+ EGL10.EGL_ALPHA_SIZE, alphaSize,
+ EGL10.EGL_DEPTH_SIZE, depthSize,
+ EGL10.EGL_STENCIL_SIZE, stencilSize,
+ EGL10.EGL_NONE});
+ mValue = new int[1];
+ mRedSize = redSize;
+ mGreenSize = greenSize;
+ mBlueSize = blueSize;
+ mAlphaSize = alphaSize;
+ mDepthSize = depthSize;
+ mStencilSize = stencilSize;
+ }
+
+ @Override
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
+ EGLConfig[] configs) {
+ for (EGLConfig config : configs) {
+ int d = findConfigAttrib(egl, display, config,
+ EGL10.EGL_DEPTH_SIZE, 0);
+ int s = findConfigAttrib(egl, display, config,
+ EGL10.EGL_STENCIL_SIZE, 0);
+ if (d >= mDepthSize && s >= mStencilSize) {
+ int r = findConfigAttrib(egl, display, config,
+ EGL10.EGL_RED_SIZE, 0);
+ int g = findConfigAttrib(egl, display, config,
+ EGL10.EGL_GREEN_SIZE, 0);
+ int b = findConfigAttrib(egl, display, config,
+ EGL10.EGL_BLUE_SIZE, 0);
+ int a = findConfigAttrib(egl, display, config,
+ EGL10.EGL_ALPHA_SIZE, 0);
+ if (r == mRedSize && g == mGreenSize
+ && b == mBlueSize && a == mAlphaSize) {
+ return config;
+ }
+ }
+ }
+ return null;
+ }
+
+ private int findConfigAttrib(EGL10 egl, EGLDisplay display,
+ EGLConfig config, int attribute, int defaultValue) {
+
+ if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
+ return mValue[0];
+ }
+ return defaultValue;
+ }
+
+ private int[] mValue;
+ /**
+ * Subclasses can adjust these values:
+ * The M red size.
+ */
+ protected int mRedSize;
+ /**
+ * The M green size.
+ */
+ protected int mGreenSize;
+ /**
+ * The M blue size.
+ */
+ protected int mBlueSize;
+ /**
+ * The M alpha size.
+ */
+ protected int mAlphaSize;
+ /**
+ * The M depth size.
+ */
+ protected int mDepthSize;
+ /**
+ * The M stencil size.
+ */
+ protected int mStencilSize;
+ }
+
+ /**
+ * This class will choose a RGB_888 surface with
+ * or without a depth buffer.
+ */
+ private final class SimpleEGLConfigChooser extends ComponentSizeChooser {
+ /**
+ * Instantiates a new Simple egl config chooser.
+ *
+ * @param withDepthBuffer the with depth buffer
+ */
+ private SimpleEGLConfigChooser(boolean withDepthBuffer) {
+ super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0);
+ }
+ }
+
+ /**
+ * An EGL helper class.
+ */
+
+ private static final class EglHelper {
+ /**
+ * Instantiates a new Egl helper.
+ *
+ * @param glTextureViewWeakRef the gl texture view weak ref
+ */
+ private EglHelper(WeakReference glTextureViewWeakRef) {
+ mGLTextureViewWeakRef = glTextureViewWeakRef;
+ }
+
+ /**
+ * Initialize EGL for a given configuration spec.
+ */
+ public void start() {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId());
+ }
+ /*
+ * Get an EGL instance
+ */
+ mEgl = (EGL10) EGLContext.getEGL();
+
+ /*
+ * Get to the default display.
+ */
+ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new RuntimeException("eglGetDisplay failed");
+ }
+
+ /*
+ * We can now initialize EGL for that display
+ */
+ int[] version = new int[2];
+ if (!mEgl.eglInitialize(mEglDisplay, version)) {
+ throw new RuntimeException("eglInitialize failed");
+ }
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view == null) {
+ mEglConfig = null;
+ mEglContext = null;
+ } else {
+ mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
+
+ /*
+ * Create an EGL context. We want to do this as rarely as we can, because an
+ * EGL context is a somewhat heavy object.
+ */
+ mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
+ }
+ if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+ mEglContext = null;
+ throwEglException("createContext", mEgl.eglGetError());
+ }
+ if (LOG_EGL) {
+ Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId());
+ }
+
+ mEglSurface = null;
+ }
+
+ /**
+ * Create an egl surface for the current SurfaceHolder surface. If a surface
+ * already exists, destroy it before creating the new surface.
+ *
+ * @return true if the surface was created successfully.
+ */
+ public boolean createSurface() {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId());
+ }
+ /*
+ * Check preconditions.
+ */
+ if (mEgl == null) {
+ throw new RuntimeException("egl not initialized");
+ }
+ if (mEglDisplay == null) {
+ throw new RuntimeException("eglDisplay not initialized");
+ }
+ if (mEglConfig == null) {
+ throw new RuntimeException("mEglConfig not initialized");
+ }
+
+ /*
+ * The window size has changed, so we need to create a new
+ * surface.
+ */
+ destroySurface();
+
+ /*
+ * Create an EGL surface we can render into.
+ */
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
+ mEglDisplay, mEglConfig, view.getSurfaceTexture());
+ } else {
+ mEglSurface = null;
+ }
+
+ if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+ int error = mEgl.eglGetError();
+ if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
+ Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+ }
+ return false;
+ }
+
+ /*
+ * Before we can issue GL commands, we need to make sure
+ * the context is current and bound to a surface.
+ */
+ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+ /*
+ * Could not make the context current, probably because the underlying
+ * SurfaceView surface has been destroyed.
+ */
+ logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a GL object for the current EGL context.
+ *
+ * @return the gl
+ */
+ GL createGL() {
+
+ GL gl = mEglContext.getGL();
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ if (view.mGLWrapper != null) {
+ gl = view.mGLWrapper.wrap(gl);
+ }
+
+ if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) {
+ int configFlags = 0;
+ Writer log = null;
+ if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) {
+ configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR;
+ }
+ if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) {
+ log = new LogWriter();
+ }
+ gl = GLDebugHelper.wrap(gl, configFlags, log);
+ }
+ }
+ return gl;
+ }
+
+ /**
+ * Display the current render surface.
+ *
+ * @return the EGL error code from eglSwapBuffers.
+ */
+ public int swap() {
+ if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+ return mEgl.eglGetError();
+ }
+ return EGL10.EGL_SUCCESS;
+ }
+
+ /**
+ * Destroy surface.
+ */
+ public void destroySurface() {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId());
+ }
+ destroySurfaceImp();
+ }
+
+ private void destroySurfaceImp() {
+ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "destroySurfaceImp() tid=" + Thread.currentThread().getId());
+ }
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_CONTEXT);
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId());
+ }
+ view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
+ }
+ mEglSurface = null;
+ }
+ }
+
+ /**
+ * Finish.
+ */
+ public void finish() {
+ if (LOG_EGL) {
+ Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId());
+ }
+ if (mEglContext != null) {
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ Log.w("EglHelper", "destroyContext() tid=" + Thread.currentThread().getId());
+ view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
+ }
+ mEglContext = null;
+ }
+ if (mEglDisplay != null) {
+ mEgl.eglTerminate(mEglDisplay);
+ mEglDisplay = null;
+ }
+ }
+
+ /**
+ * Throw egl exception.
+ *
+ * @param function the function
+ * @param error the error
+ */
+ public static void throwEglException(String function, int error) {
+ String message = formatEglError(function, error);
+ if (LOG_THREADS) {
+ Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " "
+ + message);
+ }
+ throw new RuntimeException(message);
+ }
+
+ /**
+ * Log egl error as warning.
+ *
+ * @param tag the tag
+ * @param function the function
+ * @param error the error
+ */
+ public static void logEglErrorAsWarning(String tag, String function, int error) {
+ Log.w(tag, formatEglError(function, error));
+ }
+
+ /**
+ * Format egl error string.
+ *
+ * @param function the function
+ * @param error the error
+ * @return the string
+ */
+ public static String formatEglError(String function, int error) {
+ return function + " failed: " + LogWriter.getErrorString(error);
+ }
+
+ private WeakReference mGLTextureViewWeakRef;
+ /**
+ * The M egl.
+ */
+ EGL10 mEgl;
+ /**
+ * The M egl display.
+ */
+ EGLDisplay mEglDisplay;
+ /**
+ * The M egl surface.
+ */
+ EGLSurface mEglSurface;
+ /**
+ * The M egl config.
+ */
+ EGLConfig mEglConfig;
+ /**
+ * The M egl context.
+ */
+ EGLContext mEglContext;
+
+ }
+
+ /**
+ * A generic GL Thread. Takes care of initializing EGL and GL. Delegates
+ * to a Renderer instance to do the actual drawing. Can be configured to
+ * render continuously or on request.
+ *
+ * All potentially blocking synchronization is done through the
+ * sGLThreadManager object. This avoids multiple-lock ordering issues.
+ */
+ static class GLThread extends Thread {
+ /**
+ * Instantiates a new Gl thread.
+ *
+ * @param glTextureViewWeakRef the gl texture view weak ref
+ */
+ GLThread(WeakReference glTextureViewWeakRef) {
+ super();
+ mWidth = 0;
+ mHeight = 0;
+ mRequestRender = true;
+ mRenderMode = RENDERMODE_CONTINUOUSLY;
+ mWantRenderNotification = false;
+ mGLTextureViewWeakRef = glTextureViewWeakRef;
+ }
+
+ @Override
+ public void run() {
+ setName("GLThread " + getId());
+ if (LOG_THREADS) {
+ Log.i("GLThread", "starting tid=" + getId());
+ }
+
+ try {
+ guardedRun();
+ } catch (InterruptedException e) {
+ // fall thru and exit normally
+ } finally {
+ GLOBAL_GLTHREAD_MANAGER.threadExiting(this);
+ }
+ }
+
+ /*
+ * This private method should only be called inside a
+ * synchronized(sGLThreadManager) block.
+ */
+ private void stopEglSurfaceLocked() {
+ if (mHaveEglSurface) {
+ mHaveEglSurface = false;
+ mEglHelper.destroySurface();
+ }
+ }
+
+ /*
+ * This private method should only be called inside a
+ * synchronized(sGLThreadManager) block.
+ */
+ private void stopEglContextLocked() {
+ if (mHaveEglContext) {
+ mEglHelper.finish();
+ mHaveEglContext = false;
+ GLOBAL_GLTHREAD_MANAGER.releaseEglContextLocked(this);
+ }
+ }
+
+ private void guardedRun() throws InterruptedException {
+ mEglHelper = new EglHelper(mGLTextureViewWeakRef);
+ mHaveEglContext = false;
+ mHaveEglSurface = false;
+ mWantRenderNotification = false;
+
+ try {
+ GL10 gl = null;
+ boolean createEglContext = false;
+ boolean createEglSurface = false;
+ boolean createGlInterface = false;
+ boolean lostEglContext = false;
+ boolean sizeChanged = false;
+ boolean wantRenderNotification = false;
+ boolean doRenderNotification = false;
+ boolean askedToReleaseEglContext = false;
+ int w = 0;
+ int h = 0;
+ Runnable event = null;
+ Runnable finishDrawingRunnable = null;
+
+ while (true) {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ while (true) {
+ if (mShouldExit) {
+ return;
+ }
+
+ if (!mEventQueue.isEmpty()) {
+ event = mEventQueue.remove(0);
+ break;
+ }
+
+ // Update the pause state.
+ boolean pausing = false;
+ if (mPaused != mRequestPaused) {
+ pausing = mRequestPaused;
+ mPaused = mRequestPaused;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ if (LOG_PAUSE_RESUME) {
+ Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId());
+ }
+ }
+
+ // Do we need to give up the EGL context?
+ if (mShouldReleaseEglContext) {
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "releasing EGL context because asked to tid=" + getId());
+ }
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ mShouldReleaseEglContext = false;
+ askedToReleaseEglContext = true;
+ }
+
+ // Have we lost the EGL context?
+ if (lostEglContext) {
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ lostEglContext = false;
+ }
+
+ // When pausing, release the EGL surface:
+ if (pausing && mHaveEglSurface) {
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "releasing EGL surface because paused tid=" + getId());
+ }
+ stopEglSurfaceLocked();
+ }
+
+ // When pausing, optionally release the EGL Context:
+ if (pausing && mHaveEglContext) {
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ boolean preserveEglContextOnPause = view != null && view.mPreserveEGLContextOnPause;
+ if (!preserveEglContextOnPause) {
+ stopEglContextLocked();
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "releasing EGL context because paused tid=" + getId());
+ }
+ }
+ }
+
+ // Have we lost the SurfaceView surface?
+ if (!mHasSurface && !mWaitingForSurface) {
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId());
+ }
+ if (mHaveEglSurface) {
+ stopEglSurfaceLocked();
+ }
+ mWaitingForSurface = true;
+ mSurfaceIsBad = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+
+ // Have we acquired the surface view surface?
+ if (mHasSurface && mWaitingForSurface) {
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId());
+ }
+ mWaitingForSurface = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+
+ if (doRenderNotification) {
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "sending render notification tid=" + getId());
+ }
+ mWantRenderNotification = false;
+ doRenderNotification = false;
+ mRenderComplete = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+
+ if (mFinishDrawingRunnable != null) {
+ finishDrawingRunnable = mFinishDrawingRunnable;
+ mFinishDrawingRunnable = null;
+ }
+
+ // Ready to draw?
+ if (readyToDraw()) {
+
+ // If we don't have an EGL context, try to acquire one.
+ if (!mHaveEglContext) {
+ if (askedToReleaseEglContext) {
+ askedToReleaseEglContext = false;
+ } else {
+ try {
+ mEglHelper.start();
+ } catch (RuntimeException t) {
+ GLOBAL_GLTHREAD_MANAGER.releaseEglContextLocked(this);
+ throw t;
+ }
+ mHaveEglContext = true;
+ createEglContext = true;
+
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ }
+
+ if (mHaveEglContext && !mHaveEglSurface) {
+ mHaveEglSurface = true;
+ createEglSurface = true;
+ createGlInterface = true;
+ sizeChanged = true;
+ }
+
+ if (mHaveEglSurface) {
+ if (mSizeChanged) {
+ sizeChanged = true;
+ w = mWidth;
+ h = mHeight;
+ mWantRenderNotification = true;
+ if (LOG_SURFACE) {
+ Log.i("GLThread",
+ "noticing that we want render notification tid="
+ + getId());
+ }
+
+ // Destroy and recreate the EGL surface.
+ createEglSurface = true;
+
+ mSizeChanged = false;
+ }
+ mRequestRender = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ if (mWantRenderNotification) {
+ wantRenderNotification = true;
+ }
+ break;
+ }
+ } else {
+ if (finishDrawingRunnable != null) {
+ Log.w(TAG, "Warning, !readyToDraw() but waiting for "
+ + "draw finished! Early reporting draw finished.");
+ finishDrawingRunnable.run();
+ finishDrawingRunnable = null;
+ }
+ }
+ // By design, this is the only place in a GLThread thread where we wait().
+ if (LOG_THREADS) {
+ Log.i("GLThread", "waiting tid=" + getId()
+ + " mHaveEglContext: " + mHaveEglContext
+ + " mHaveEglSurface: " + mHaveEglSurface
+ + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface
+ + " mPaused: " + mPaused
+ + " mHasSurface: " + mHasSurface
+ + " mSurfaceIsBad: " + mSurfaceIsBad
+ + " mWaitingForSurface: " + mWaitingForSurface
+ + " mWidth: " + mWidth
+ + " mHeight: " + mHeight
+ + " mRequestRender: " + mRequestRender
+ + " mRenderMode: " + mRenderMode);
+ }
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ }
+ } // end of synchronized(sGLThreadManager)
+
+ if (event != null) {
+ event.run();
+ event = null;
+ continue;
+ }
+
+ if (createEglSurface) {
+ if (LOG_SURFACE) {
+ Log.w("GLThread", "egl createSurface");
+ }
+ if (mEglHelper.createSurface()) {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mFinishedCreatingEglSurface = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ } else {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mFinishedCreatingEglSurface = true;
+ mSurfaceIsBad = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ continue;
+ }
+ createEglSurface = false;
+ }
+
+ if (createGlInterface) {
+ gl = (GL10) mEglHelper.createGL();
+
+ createGlInterface = false;
+ }
+
+ if (createEglContext) {
+ if (LOG_RENDERER) {
+ Log.w("GLThread", "onSurfaceCreated");
+ }
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
+ }
+ createEglContext = false;
+ }
+
+ if (sizeChanged) {
+ if (LOG_RENDERER) {
+ Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")");
+ }
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ view.mRenderer.onSurfaceChanged(gl, w, h);
+ }
+ sizeChanged = false;
+ }
+
+ if (LOG_RENDERER_DRAW_FRAME) {
+ Log.w("GLThread", "onDrawFrame tid=" + getId());
+ }
+ GLTextureView view = mGLTextureViewWeakRef.get();
+ if (view != null) {
+ view.mRenderer.onDrawFrame(gl);
+ if (finishDrawingRunnable != null) {
+ finishDrawingRunnable.run();
+ finishDrawingRunnable = null;
+ }
+ }
+ int swapError = mEglHelper.swap();
+ switch (swapError) {
+ case EGL10.EGL_SUCCESS:
+ break;
+ case EGL11.EGL_CONTEXT_LOST:
+ if (LOG_SURFACE) {
+ Log.i("GLThread", "egl context lost tid=" + getId());
+ }
+ lostEglContext = true;
+ break;
+ default:
+ // Other errors typically mean that the current surface is bad,
+ // probably because the SurfaceView surface has been destroyed,
+ // but we haven't been notified yet.
+ // Log the error to help developers understand why rendering stopped.
+ EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError);
+
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mSurfaceIsBad = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ break;
+ }
+
+ if (wantRenderNotification) {
+ doRenderNotification = true;
+ wantRenderNotification = false;
+ }
+ }
+
+ } finally {
+ /*
+ * clean-up everything...
+ */
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ stopEglSurfaceLocked();
+ stopEglContextLocked();
+ }
+ }
+ }
+
+ /**
+ * Able to draw boolean.
+ *
+ * @return the boolean
+ */
+ public boolean ableToDraw() {
+ return mHaveEglContext && mHaveEglSurface && readyToDraw();
+ }
+
+ /**
+ * Ready to draw boolean.
+ *
+ * @return the boolean
+ */
+ public boolean readyToDraw() {
+ return !mPaused
+ && mHasSurface
+ && !mSurfaceIsBad
+ && mWidth > 0
+ && mHeight > 0
+ && mRequestRender
+ || mRenderMode == RENDERMODE_CONTINUOUSLY;
+ }
+
+ /**
+ * Sets render mode.
+ *
+ * @param renderMode the render mode
+ */
+ public void setRenderMode(int renderMode) {
+ if (!(RENDERMODE_WHEN_DIRTY <= renderMode && renderMode <= RENDERMODE_CONTINUOUSLY)) {
+ throw new IllegalArgumentException("renderMode");
+ }
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mRenderMode = renderMode;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ }
+
+ /**
+ * Gets render mode.
+ *
+ * @return the render mode
+ */
+ public int getRenderMode() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ return mRenderMode;
+ }
+ }
+
+ /**
+ * Request render.
+ */
+ public void requestRender() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mRequestRender = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ }
+
+ /**
+ * Request render and notify.
+ *
+ * @param finishDrawing the finish drawing
+ */
+ public void requestRenderAndNotify(Runnable finishDrawing) {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ // If we are already on the GL thread, this means a client callback
+ // has caused reentrancy, for example via updating the SurfaceView parameters.
+ // We will return to the client rendering code, so here we don't need to
+ // do anything.
+ if (Thread.currentThread() == this) {
+ return;
+ }
+
+ mWantRenderNotification = true;
+ mRequestRender = true;
+ mRenderComplete = false;
+ mFinishDrawingRunnable = finishDrawing;
+
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ }
+
+ /**
+ * Surface created.
+ */
+ public void surfaceCreated() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ if (LOG_THREADS) {
+ Log.i("GLThread", "surfaceCreated tid=" + getId());
+ }
+ mHasSurface = true;
+ mFinishedCreatingEglSurface = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ while (mWaitingForSurface
+ && !mFinishedCreatingEglSurface
+ && !mExited) {
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * Surface destroyed.
+ */
+ public void surfaceDestroyed() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ if (LOG_THREADS) {
+ Log.i("GLThread", "surfaceDestroyed tid=" + getId());
+ }
+ mHasSurface = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ while (!mWaitingForSurface && !mExited) {
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * On pause.
+ */
+ public void onPause() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ if (LOG_PAUSE_RESUME) {
+ Log.i("GLThread", "onPause tid=" + getId());
+ }
+ mRequestPaused = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ while (!mExited && !mPaused) {
+ if (LOG_PAUSE_RESUME) {
+ Log.i("Main thread", "onPause waiting for mPaused.");
+ }
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * On resume.
+ */
+ public void onResume() {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ if (LOG_PAUSE_RESUME) {
+ Log.i("GLThread", "onResume tid=" + getId());
+ }
+ mRequestPaused = false;
+ mRequestRender = true;
+ mRenderComplete = false;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ while (!mExited && mPaused && !mRenderComplete) {
+ if (LOG_PAUSE_RESUME) {
+ Log.i("Main thread", "onResume waiting for !mPaused.");
+ }
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * On window resize.
+ *
+ * @param w the w
+ * @param h the h
+ */
+ public void onWindowResize(int w, int h) {
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mWidth = w;
+ mHeight = h;
+ mSizeChanged = true;
+ mRequestRender = true;
+ mRenderComplete = false;
+
+ // If we are already on the GL thread, this means a client callback
+ // has caused reentrancy, for example via updating the SurfaceView parameters.
+ // We need to process the size change eventually though and update our EGLSurface.
+ // So we set the parameters and return so they can be processed on our
+ // next iteration.
+ if (Thread.currentThread() == this) {
+ return;
+ }
+
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+
+ // Wait for thread to react to resize and render a frame
+ while (!mExited && !mPaused && !mRenderComplete
+ && ableToDraw()) {
+ if (LOG_SURFACE) {
+ Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId());
+ }
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * Request exit and wait.
+ */
+ public void requestExitAndWait() {
+ // don't call this from GLThread thread or it is a guaranteed
+ // deadlock!
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mShouldExit = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ while (!mExited) {
+ try {
+ GLOBAL_GLTHREAD_MANAGER.wait();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * Request release egl context locked.
+ */
+ public void requestReleaseEglContextLocked() {
+ mShouldReleaseEglContext = true;
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+
+ /**
+ * Queue an "event" to be run on the GL rendering thread.
+ *
+ * @param r the runnable to be run on the GL rendering thread.
+ */
+ public void queueEvent(Runnable r) {
+ if (r == null) {
+ throw new IllegalArgumentException("r must not be null");
+ }
+ synchronized (GLOBAL_GLTHREAD_MANAGER) {
+ mEventQueue.add(r);
+ GLOBAL_GLTHREAD_MANAGER.notifyAll();
+ }
+ }
+
+ /**
+ * Gets egl context.
+ *
+ * @return the egl context
+ */
+ public EGLContext getEglContext() {
+ if (mEglHelper.mEglContext == null || EGL10.EGL_NO_CONTEXT == mEglHelper.mEglContext) {
+ Log.i("GLThread", "getEglContext mEglContext is invalid.");
+ mEglHelper.start();
+ }
+ return mEglHelper.mEglContext;
+ }
+
+ // Once the thread is started, all accesses to the following member
+ // variables are protected by the sGLThreadManager monitor
+ private boolean mShouldExit;
+ private boolean mExited;
+ private boolean mRequestPaused;
+ private boolean mPaused;
+ private boolean mHasSurface;
+ private boolean mSurfaceIsBad;
+ private boolean mWaitingForSurface;
+ private boolean mHaveEglContext;
+ private boolean mHaveEglSurface;
+ private boolean mFinishedCreatingEglSurface;
+ private boolean mShouldReleaseEglContext;
+ private int mWidth;
+ private int mHeight;
+ private int mRenderMode;
+ private boolean mRequestRender;
+ private boolean mWantRenderNotification;
+ private boolean mRenderComplete;
+ private ArrayList mEventQueue = new ArrayList();
+ private boolean mSizeChanged = true;
+ private Runnable mFinishDrawingRunnable = null;
+
+ // End of member variables protected by the sGLThreadManager monitor.
+
+ private EglHelper mEglHelper;
+
+ /**
+ * Set once at thread construction time, null out when the parent view is garbage
+ * called. This weak reference allows the GLSurfaceView to be garbage collected while
+ * the GLThread is still alive.
+ */
+ private WeakReference mGLTextureViewWeakRef;
+
+ }
+
+ /**
+ * The type Log writer.
+ */
+ static class LogWriter extends Writer {
+
+ @Override
+ public void close() {
+ flushBuilder();
+ }
+
+ @Override
+ public void flush() {
+ flushBuilder();
+ }
+
+ @Override
+ public void write(char[] buf, int offset, int count) {
+ for (int i = 0; i < count; i++) {
+ char c = buf[offset + i];
+ if (c == '\n') {
+ flushBuilder();
+ } else {
+ mBuilder.append(c);
+ }
+ }
+ }
+
+ private void flushBuilder() {
+ if (mBuilder.length() > 0) {
+ Log.v("GLSurfaceView", mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+ }
+ }
+
+ /**
+ * Gets error string.
+ *
+ * @param error the error
+ * @return the error string
+ */
+ public static String getErrorString(int error) {
+ switch (error) {
+ case EGL_SUCCESS:
+ return "EGL_SUCCESS";
+ case EGL_NOT_INITIALIZED:
+ return "EGL_NOT_INITIALIZED";
+ case EGL_BAD_ACCESS:
+ return "EGL_BAD_ACCESS";
+ case EGL_BAD_ALLOC:
+ return "EGL_BAD_ALLOC";
+ case EGL_BAD_ATTRIBUTE:
+ return "EGL_BAD_ATTRIBUTE";
+ case EGL_BAD_CONFIG:
+ return "EGL_BAD_CONFIG";
+ case EGL_BAD_CONTEXT:
+ return "EGL_BAD_CONTEXT";
+ case EGL_BAD_CURRENT_SURFACE:
+ return "EGL_BAD_CURRENT_SURFACE";
+ case EGL_BAD_DISPLAY:
+ return "EGL_BAD_DISPLAY";
+ case EGL_BAD_MATCH:
+ return "EGL_BAD_MATCH";
+ case EGL_BAD_NATIVE_PIXMAP:
+ return "EGL_BAD_NATIVE_PIXMAP";
+ case EGL_BAD_NATIVE_WINDOW:
+ return "EGL_BAD_NATIVE_WINDOW";
+ case EGL_BAD_PARAMETER:
+ return "EGL_BAD_PARAMETER";
+ case EGL_BAD_SURFACE:
+ return "EGL_BAD_SURFACE";
+ case EGL11.EGL_CONTEXT_LOST:
+ return "EGL_CONTEXT_LOST";
+ default:
+ return getHex(error);
+ }
+ }
+
+ private static String getHex(int value) {
+ return "0x" + Integer.toHexString(value);
+ }
+
+ private StringBuilder mBuilder = new StringBuilder();
+ }
+
+ private void checkRenderThreadState() {
+ if (mGLThread != null) {
+ throw new IllegalStateException(
+ "setRenderer has already been called for this instance.");
+ }
+ }
+
+ private static final class GLThreadManager {
+
+ /**
+ * Thread exiting.
+ *
+ * @param thread the thread
+ */
+ public synchronized void threadExiting(GLThread thread) {
+ if (LOG_THREADS) {
+ Log.i("GLThread", "exiting tid=" + thread.getId());
+ }
+ thread.mExited = true;
+ notifyAll();
+ }
+
+ /**
+ * Release egl context locked.
+ *
+ * @param thread the thread
+ */
+ /*
+ * Releases the EGL context. Requires that we are already in the
+ * sGLThreadManager monitor when this is called.
+ */
+ public void releaseEglContextLocked(GLThread thread) {
+ notifyAll();
+ }
+ }
+
+ private static final GLThreadManager GLOBAL_GLTHREAD_MANAGER = new GLThreadManager();
+
+
+ private final WeakReference mThisWeakRef =
+ new WeakReference(this);
+ private GLThread mGLThread;
+ private Renderer mRenderer;
+ private boolean mDetached;
+ private EGLConfigChooser mEGLConfigChooser;
+ private EGLContextFactory mEGLContextFactory;
+ private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
+ private GLWrapper mGLWrapper;
+ private int mDebugFlags;
+ private int mEGLContextClientVersion;
+ private boolean mPreserveEGLContextOnPause;
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java
new file mode 100644
index 000000000..8788511e5
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java
@@ -0,0 +1,148 @@
+package io.agora.api.example.compose.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import io.agora.api.example.compose.BuildConfig;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+/**
+ * The type Token utils.
+ */
+public final class TokenUtils {
+ private static final String TAG = "TokenGenerator";
+ private final static OkHttpClient CLIENT;
+
+ private TokenUtils() {
+
+ }
+
+ static {
+ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
+ interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+ CLIENT = new OkHttpClient.Builder()
+ .addInterceptor(interceptor)
+ .build();
+ }
+
+ /**
+ * Gen.
+ *
+ * @param channelName the channel name
+ * @param uid the uid
+ * @param onGetToken the on get token
+ */
+ public static void gen(String channelName, int uid, OnTokenGenCallback onGetToken) {
+ gen(BuildConfig.AGORA_APP_ID, BuildConfig.AGORA_APP_CERT, channelName, uid, ret -> {
+ if (onGetToken != null) {
+ runOnUiThread(() -> {
+ onGetToken.onTokenGen(ret);
+ });
+ }
+ }, ret -> {
+ Log.e(TAG, "for requesting token error.", ret);
+ if (onGetToken != null) {
+ runOnUiThread(() -> {
+ onGetToken.onTokenGen(null);
+ });
+ }
+ });
+ }
+
+ private static void runOnUiThread(@NonNull Runnable runnable) {
+ if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
+ runnable.run();
+ } else {
+ new Handler(Looper.getMainLooper()).post(runnable);
+ }
+ }
+
+ private static void gen(String appId, String certificate, String channelName, int uid, OnTokenGenCallback onGetToken, OnTokenGenCallback onError) {
+ if (TextUtils.isEmpty(appId) || TextUtils.isEmpty(certificate) || TextUtils.isEmpty(channelName)) {
+ if (onError != null) {
+ onError.onTokenGen(new IllegalArgumentException("appId=" + appId + ", certificate=" + certificate + ", channelName=" + channelName));
+ }
+ return;
+ }
+ JSONObject postBody = new JSONObject();
+ try {
+ postBody.put("appId", appId);
+ postBody.put("appCertificate", certificate);
+ postBody.put("channelName", channelName);
+ postBody.put("expire", 900); // s
+ postBody.put("src", "Android");
+ postBody.put("ts", System.currentTimeMillis() + "");
+ postBody.put("type", 1); // 1: RTC Token ; 2: RTM Token
+ postBody.put("uid", uid + "");
+ } catch (JSONException e) {
+ if (onError != null) {
+ onError.onTokenGen(e);
+ }
+ }
+
+ Request request = new Request.Builder()
+ .url("https://test-toolbox.bj2.agoralab.co/v1/token/generate")
+ .addHeader("Content-Type", "application/json")
+ .post(RequestBody.create(postBody.toString(), null))
+ .build();
+ CLIENT.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ if (onError != null) {
+ onError.onTokenGen(e);
+ }
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
+ ResponseBody body = response.body();
+ if (body != null) {
+ try {
+ JSONObject jsonObject = new JSONObject(body.string());
+ JSONObject data = jsonObject.optJSONObject("data");
+ String token = Objects.requireNonNull(data).optString("token");
+ if (onGetToken != null) {
+ onGetToken.onTokenGen(token);
+ }
+ } catch (Exception e) {
+ if (onError != null) {
+ onError.onTokenGen(e);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * The interface On token gen callback.
+ *
+ * @param the type parameter
+ */
+ public interface OnTokenGenCallback {
+ /**
+ * On token gen.
+ *
+ * @param token the token
+ */
+ void onTokenGen(T token);
+ }
+
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/VideoFileReader.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/VideoFileReader.java
new file mode 100644
index 000000000..acc07e1b4
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/VideoFileReader.java
@@ -0,0 +1,147 @@
+package io.agora.api.example.compose.utils;
+
+import android.content.Context;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * The type Video file reader.
+ */
+public class VideoFileReader {
+ private final String RAW_VIDEO_PATH = "sample.yuv";
+ private final int RAW_VIDEO_WIDTH = 320;
+ private final int RAW_VIDEO_HEIGHT = 180;
+ private final int RAW_VIDEO_FRAME_SIZE = RAW_VIDEO_WIDTH * RAW_VIDEO_HEIGHT / 2 * 3;
+ private final int RAW_VIDEO_FRAME_RATE = 15;
+ private final long RAW_VIDEO_FRAME_INTERVAL_NS = 1000 * 1000 * 1000 / RAW_VIDEO_FRAME_RATE;
+
+ private volatile boolean pushing = false;
+ private InputStream inputStream;
+
+ private final Context context;
+ private final OnVideoReadListener videoReadListener;
+ private InnerThread thread;
+ private final int trackId;
+
+ /**
+ * Instantiates a new Video file reader.
+ *
+ * @param context the context
+ * @param listener the listener
+ */
+ public VideoFileReader(Context context, OnVideoReadListener listener) {
+ this(context, 0, listener);
+ }
+
+ /**
+ * Instantiates a new Video file reader.
+ *
+ * @param context the context
+ * @param trackId the track id
+ * @param listener the listener
+ */
+ public VideoFileReader(Context context, int trackId, OnVideoReadListener listener) {
+ this.trackId = trackId;
+ this.context = context.getApplicationContext();
+ this.videoReadListener = listener;
+ }
+
+ /**
+ * Gets track id.
+ *
+ * @return the track id
+ */
+ public int getTrackId() {
+ return trackId;
+ }
+
+ /**
+ * Start.
+ */
+ public final void start() {
+ if (thread != null) {
+ return;
+ }
+ thread = new InnerThread();
+ thread.start();
+ }
+
+ /**
+ * Stop.
+ */
+ public final void stop() {
+ if (thread != null) {
+ pushing = false;
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ thread = null;
+ }
+ }
+ }
+
+
+ /**
+ * The interface On video read listener.
+ */
+ public interface OnVideoReadListener {
+ /**
+ * On video read.
+ *
+ * @param buffer the buffer
+ * @param width the width
+ * @param height the height
+ */
+ void onVideoRead(byte[] buffer, int width, int height);
+ }
+
+ private final class InnerThread extends Thread {
+ @Override
+ public void run() {
+ super.run();
+ try {
+ inputStream = context.getAssets().open(RAW_VIDEO_PATH);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ pushing = true;
+ byte[] buffer = new byte[RAW_VIDEO_FRAME_SIZE];
+ while (pushing) {
+ long start = System.nanoTime();
+ try {
+ int read = inputStream.read(buffer);
+ while (read < 0) {
+ inputStream.reset();
+ read = inputStream.read(buffer);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if (videoReadListener != null) {
+ videoReadListener.onVideoRead(buffer, RAW_VIDEO_WIDTH, RAW_VIDEO_HEIGHT);
+ }
+ long consume = System.nanoTime() - start;
+
+ try {
+ Thread.sleep(Math.max(0, (RAW_VIDEO_FRAME_INTERVAL_NS - consume) / 1000 / 1000));
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ inputStream = null;
+ }
+ }
+ }
+ }
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YUVUtils.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YUVUtils.java
new file mode 100644
index 000000000..7ab72bfef
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YUVUtils.java
@@ -0,0 +1,308 @@
+package io.agora.api.example.compose.utils;
+
+import static android.renderscript.Element.RGBA_8888;
+import static android.renderscript.Element.U8;
+import static android.renderscript.Element.U8_4;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.renderscript.Allocation;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.renderscript.ScriptIntrinsicYuvToRGB;
+import android.renderscript.Type.Builder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * The type Yuv utils.
+ */
+public final class YUVUtils {
+
+ private YUVUtils() {
+
+ }
+
+ /**
+ * Encode i 420.
+ *
+ * @param i420 the 420
+ * @param argb the argb
+ * @param width the width
+ * @param height the height
+ */
+ public static void encodeI420(byte[] i420, int[] argb, int width, int height) {
+ final int frameSize = width * height;
+
+ int yIndex = 0; // Y start index
+ int uIndex = frameSize; // U statt index
+ int vIndex = frameSize * 5 / 4; // v start index: w*h*5/4
+
+ int r, g, b, y, u, v;
+ int index = 0;
+ for (int j = 0; j < height; j++) {
+ for (int i = 0; i < width; i++) {
+ r = (argb[index] & 0xff0000) >> 16;
+ g = (argb[index] & 0xff00) >> 8;
+ b = (argb[index] & 0xff) >> 0;
+
+ // well known RGB to YUV algorithm
+ y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
+ u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
+ v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
+
+ // I420(YUV420p) -> YYYYYYYY UU VV
+ i420[yIndex++] = (byte) ((y < 0) ? 0 : ((y > 255) ? 255 : y));
+ if (j % 2 == 0 && i % 2 == 0) {
+ i420[uIndex++] = (byte) ((u < 0) ? 0 : ((u > 255) ? 255 : u));
+ i420[vIndex++] = (byte) ((v < 0) ? 0 : ((v > 255) ? 255 : v));
+ }
+ index++;
+ }
+ }
+ }
+
+ /**
+ * Encode nv 21.
+ *
+ * @param yuv420sp the yuv 420 sp
+ * @param argb the argb
+ * @param width the width
+ * @param height the height
+ */
+ public static void encodeNV21(byte[] yuv420sp, int[] argb, int width, int height) {
+ final int frameSize = width * height;
+
+ int yIndex = 0;
+ int uvIndex = frameSize;
+
+ int r, g, b, y, u, v;
+ int index = 0;
+ for (int j = 0; j < height; j++) {
+ for (int i = 0; i < width; i++) {
+ r = (argb[index] & 0xff0000) >> 16;
+ g = (argb[index] & 0xff00) >> 8;
+ b = (argb[index] & 0xff) >> 0;
+
+ // well known RGB to YUV algorithm
+ y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
+ u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
+ v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
+
+ // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
+ // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
+ // pixel AND every other scanline.
+ yuv420sp[yIndex++] = (byte) ((y < 0) ? 0 : ((y > 255) ? 255 : y));
+ if (j % 2 == 0 && index % 2 == 0) {
+ yuv420sp[uvIndex++] = (byte) ((v < 0) ? 0 : ((v > 255) ? 255 : v));
+ yuv420sp[uvIndex++] = (byte) ((u < 0) ? 0 : ((u > 255) ? 255 : u));
+ }
+ index++;
+ }
+ }
+ }
+
+ /**
+ * Swap yu 12 to yuv 420 sp.
+ *
+ * @param yu12bytes the yu 12 bytes
+ * @param i420bytes the 420 bytes
+ * @param width the width
+ * @param height the height
+ * @param yStride the y stride
+ * @param uStride the u stride
+ * @param vStride the v stride
+ */
+ public static void swapYU12toYUV420SP(byte[] yu12bytes, byte[] i420bytes, int width, int height, int yStride, int uStride, int vStride) {
+ System.arraycopy(yu12bytes, 0, i420bytes, 0, yStride * height);
+ int startPos = yStride * height;
+ int yv_start_pos_u = startPos;
+ int yv_start_pos_v = startPos + startPos / 4;
+ for (int i = 0; i < startPos / 4; i++) {
+ i420bytes[startPos + 2 * i + 0] = yu12bytes[yv_start_pos_v + i];
+ i420bytes[startPos + 2 * i + 1] = yu12bytes[yv_start_pos_u + i];
+ }
+ }
+
+ /**
+ * 420 to bitmap bitmap.
+ *
+ * @param width the width
+ * @param height the height
+ * @param rotation the rotation
+ * @param bufferLength the buffer length
+ * @param buffer the buffer
+ * @param yStride the y stride
+ * @param uStride the u stride
+ * @param vStride the v stride
+ * @return the bitmap
+ */
+ public static Bitmap i420ToBitmap(int width, int height, int rotation,
+ int bufferLength, byte[] buffer,
+ int yStride, int uStride, int vStride) {
+ byte[] nv21 = new byte[bufferLength];
+ swapYU12toYUV420SP(buffer, nv21, width, height, yStride, uStride, vStride);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ int[] strides = {yStride, yStride};
+ YuvImage image = new YuvImage(nv21, ImageFormat.NV21, width, height, strides);
+
+ image.compressToJpeg(
+ new Rect(0, 0, image.getWidth(), image.getHeight()),
+ 100, baos);
+
+ // rotate picture when saving to file
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rotation);
+ byte[] bytes = baos.toByteArray();
+ try {
+ baos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Blur bitmap.
+ *
+ * @param context the context
+ * @param image the image
+ * @param radius the radius
+ * @return the bitmap
+ */
+ public static Bitmap blur(Context context, Bitmap image, float radius) {
+ RenderScript rs = RenderScript.create(context);
+ Bitmap outputBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
+ Allocation in = Allocation.createFromBitmap(rs, image);
+ Allocation out = Allocation.createFromBitmap(rs, outputBitmap);
+ ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, U8_4(rs));
+ intrinsicBlur.setRadius(radius);
+ intrinsicBlur.setInput(in);
+ intrinsicBlur.forEach(out);
+
+ out.copyTo(outputBitmap);
+ image.recycle();
+ rs.destroy();
+
+ return outputBitmap;
+ }
+
+ /**
+ * Bitmap to i 420 byte [ ].
+ *
+ * @param inputWidth the input width
+ * @param inputHeight the input height
+ * @param scaled the scaled
+ * @return the byte [ ]
+ */
+ public static byte[] bitmapToI420(int inputWidth, int inputHeight, Bitmap scaled) {
+ int[] argb = new int[inputWidth * inputHeight];
+ scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);
+ byte[] yuv = new byte[inputWidth * inputHeight * 3 / 2];
+ YUVUtils.encodeI420(yuv, argb, inputWidth, inputHeight);
+ scaled.recycle();
+ return yuv;
+ }
+
+ /**
+ * To wrapped i 420 byte [ ].
+ *
+ * @param bufferY the buffer y
+ * @param bufferU the buffer u
+ * @param bufferV the buffer v
+ * @param width the width
+ * @param height the height
+ * @return the byte [ ]
+ */
+ public static byte[] toWrappedI420(ByteBuffer bufferY,
+ ByteBuffer bufferU,
+ ByteBuffer bufferV,
+ int width,
+ int height) {
+ int chromaWidth = (width + 1) / 2;
+ int chromaHeight = (height + 1) / 2;
+ int lengthY = width * height;
+ int lengthU = chromaWidth * chromaHeight;
+ int lengthV = lengthU;
+
+
+ int size = lengthY + lengthU + lengthV;
+
+ byte[] out = new byte[size];
+
+ for (int i = 0; i < size; i++) {
+ if (i < lengthY) {
+ out[i] = bufferY.get(i);
+ } else if (i < lengthY + lengthU) {
+ int j = (i - lengthY) / chromaWidth;
+ int k = (i - lengthY) % chromaWidth;
+ out[i] = bufferU.get(j * width + k);
+ } else {
+ int j = (i - lengthY - lengthU) / chromaWidth;
+ int k = (i - lengthY - lengthU) % chromaWidth;
+ out[i] = bufferV.get(j * width + k);
+ }
+ }
+
+ return out;
+ }
+
+ /**
+ * I420转nv21
+ *
+ * @param data the data
+ * @param width the width
+ * @param height the height
+ * @return the byte [ ]
+ */
+ public static byte[] i420ToNV21(byte[] data, int width, int height) {
+ byte[] ret = new byte[data.length];
+ int total = width * height;
+
+ ByteBuffer bufferY = ByteBuffer.wrap(ret, 0, total);
+ ByteBuffer bufferVU = ByteBuffer.wrap(ret, total, total / 2);
+
+ bufferY.put(data, 0, total);
+ for (int i = 0; i < total / 4; i += 1) {
+ bufferVU.put(data[i + total + total / 4]);
+ bufferVU.put(data[total + i]);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Nv 21 to bitmap bitmap.
+ *
+ * @param context the context
+ * @param nv21 the nv 21
+ * @param width the width
+ * @param height the height
+ * @return the bitmap
+ */
+ public static Bitmap nv21ToBitmap(Context context, byte[] nv21, int width, int height) {
+ RenderScript rs = RenderScript.create(context);
+ ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, U8_4(rs));
+ Builder yuvType = null;
+ yuvType = (new Builder(rs, U8(rs))).setX(nv21.length);
+ Allocation in = Allocation.createTyped(rs, yuvType.create(), 1);
+ Builder rgbaType = (new Builder(rs, RGBA_8888(rs))).setX(width).setY(height);
+ Allocation out = Allocation.createTyped(rs, rgbaType.create(), 1);
+ in.copyFrom(nv21);
+ yuvToRgbIntrinsic.setInput(in);
+ yuvToRgbIntrinsic.forEach(out);
+ Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ out.copyTo(bmpout);
+ return bmpout;
+ }
+
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvFboProgram.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvFboProgram.java
new file mode 100644
index 000000000..bf3d6dfb4
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvFboProgram.java
@@ -0,0 +1,108 @@
+package io.agora.api.example.compose.utils;
+
+import android.graphics.Matrix;
+import android.opengl.GLES20;
+
+import io.agora.base.JavaI420Buffer;
+import io.agora.base.internal.video.GlRectDrawer;
+import io.agora.base.internal.video.GlUtil;
+import io.agora.base.internal.video.RendererCommon;
+
+/**
+ * The type Yuv fbo program.
+ */
+public class YuvFboProgram {
+
+ private int[] mFboTextureId;
+ private final YuvUploader yuvUploader;
+ private final GlRectDrawer glRectDrawer;
+
+ private int mWidth, mHeight;
+ private volatile boolean isRelease;
+
+ /**
+ * Instantiates a new Yuv fbo program.
+ */
+// GL Thread
+ public YuvFboProgram() {
+ yuvUploader = new YuvUploader();
+ glRectDrawer = new GlRectDrawer();
+ }
+
+ /**
+ * Release.
+ */
+// GL Thread
+ public void release() {
+ isRelease = true;
+ if (mFboTextureId != null) {
+ GLES20.glDeleteFramebuffers(1, mFboTextureId, 0);
+ GLES20.glDeleteTextures(1, mFboTextureId, 1);
+ yuvUploader.release();
+ glRectDrawer.release();
+ mFboTextureId = null;
+ }
+ }
+
+ /**
+ * Draw yuv integer.
+ *
+ * @param yuv the yuv
+ * @param width the width
+ * @param height the height
+ * @return the integer
+ */
+// GL Thread
+ public Integer drawYuv(byte[] yuv, int width, int height) {
+ if (isRelease) {
+ return -1;
+ }
+ if (mFboTextureId == null) {
+ mFboTextureId = new int[2];
+ GLES20.glGenFramebuffers(1, mFboTextureId, 0);
+ int fboId = mFboTextureId[0];
+
+ int texture = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);
+ mFboTextureId[1] = texture;
+
+ mWidth = width;
+ mHeight = height;
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
+ GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
+ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
+ GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_TEXTURE_2D, texture, 0);
+ } else if (mWidth != width || mHeight != height) {
+ GLES20.glDeleteFramebuffers(1, mFboTextureId, 0);
+ GLES20.glDeleteTextures(1, mFboTextureId, 1);
+ mFboTextureId = null;
+ return drawYuv(yuv, width, height);
+ } else {
+ int fboId = mFboTextureId[0];
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
+ }
+ GLES20.glViewport(0, 0, mWidth, mHeight);
+
+ JavaI420Buffer i420Buffer = JavaI420Buffer.allocate(width, height);
+ i420Buffer.getDataY().put(yuv, 0, i420Buffer.getDataY().limit());
+ i420Buffer.getDataU().put(yuv, i420Buffer.getDataY().limit(), i420Buffer.getDataU().limit());
+ i420Buffer.getDataV().put(yuv, i420Buffer.getDataY().limit() + i420Buffer.getDataU().limit(), i420Buffer.getDataV().limit());
+
+ yuvUploader.uploadFromBuffer(i420Buffer);
+ Matrix matrix = new Matrix();
+ matrix.preTranslate(0.5f, 0.5f);
+ matrix.preScale(1f, -1f); // I420-frames are upside down
+ matrix.preTranslate(-0.5f, -0.5f);
+ glRectDrawer.drawYuv(yuvUploader.getYuvTextures(), RendererCommon.convertMatrixFromAndroidGraphicsMatrix(matrix), width, height, 0, 0, width, height);
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+ GLES20.glFlush();
+
+ return mFboTextureId[1];
+ }
+
+
+}
diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvUploader.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvUploader.java
new file mode 100644
index 000000000..4f594e22f
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/YuvUploader.java
@@ -0,0 +1,109 @@
+package io.agora.api.example.compose.utils;
+
+import android.opengl.GLES20;
+
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+
+import io.agora.base.VideoFrame;
+import io.agora.base.internal.video.GlUtil;
+import io.agora.base.internal.video.YuvHelper;
+
+/**
+ * The type Yuv uploader.
+ */
+public class YuvUploader {
+ // Intermediate copy buffer for uploading yuv frames that are not packed, i.e. stride > width.
+ // TODO(magjed): Investigate when GL_UNPACK_ROW_LENGTH is available, or make a custom shader
+ // that handles stride and compare performance with intermediate copy.
+ @Nullable private ByteBuffer copyBuffer;
+ @Nullable private int[] yuvTextures;
+
+ /**
+ * Upload |planes| into OpenGL textures, taking stride into consideration.
+ *
+ * @param width the width
+ * @param height the height
+ * @param strides the strides
+ * @param planes the planes
+ * @return Array of three texture indices corresponding to Y-, U-, and V-plane respectively.
+ */
+ @Nullable
+ public int[] uploadYuvData(int width, int height, int[] strides, ByteBuffer[] planes) {
+ final int[] planeWidths = new int[] {width, width / 2, width / 2};
+ final int[] planeHeights = new int[] {height, height / 2, height / 2};
+ // Make a first pass to see if we need a temporary copy buffer.
+ int copyCapacityNeeded = 0;
+ for (int i = 0; i < 3; ++i) {
+ if (strides[i] > planeWidths[i]) {
+ copyCapacityNeeded = Math.max(copyCapacityNeeded, planeWidths[i] * planeHeights[i]);
+ }
+ }
+ // Allocate copy buffer if necessary.
+ if (copyCapacityNeeded > 0
+ && (copyBuffer == null || copyBuffer.capacity() < copyCapacityNeeded)) {
+ copyBuffer = ByteBuffer.allocateDirect(copyCapacityNeeded);
+ }
+ // Make sure YUV textures are allocated.
+ if (yuvTextures == null) {
+ yuvTextures = new int[3];
+ for (int i = 0; i < 3; i++) {
+ yuvTextures[i] = GlUtil.generateTexture(GLES20.GL_TEXTURE_2D);
+ }
+ }
+ // Upload each plane.
+ for (int i = 0; i < 3; ++i) {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);
+ // GLES only accepts packed data, i.e. stride == planeWidth.
+ final ByteBuffer packedByteBuffer;
+ if (strides[i] == planeWidths[i]) {
+ // Input is packed already.
+ packedByteBuffer = planes[i];
+ } else {
+ YuvHelper.copyPlane(
+ planes[i], strides[i], copyBuffer, planeWidths[i], planeWidths[i], planeHeights[i]);
+ packedByteBuffer = copyBuffer;
+ }
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, planeWidths[i],
+ planeHeights[i], 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, packedByteBuffer);
+ }
+ return yuvTextures;
+ }
+
+ /**
+ * Upload from buffer int [ ].
+ *
+ * @param buffer the buffer
+ * @return the int [ ]
+ */
+ @Nullable
+ public int[] uploadFromBuffer(VideoFrame.I420Buffer buffer) {
+ int[] strides = {buffer.getStrideY(), buffer.getStrideU(), buffer.getStrideV()};
+ ByteBuffer[] planes = {buffer.getDataY(), buffer.getDataU(), buffer.getDataV()};
+ return uploadYuvData(buffer.getWidth(), buffer.getHeight(), strides, planes);
+ }
+
+ /**
+ * Get yuv textures int [ ].
+ *
+ * @return the int [ ]
+ */
+ @Nullable
+ public int[] getYuvTextures() {
+ return yuvTextures;
+ }
+
+ /**
+ * Releases cached resources. Uploader can still be used and the resources will be reallocated
+ * on first use.
+ */
+ public void release() {
+ copyBuffer = null;
+ if (yuvTextures != null) {
+ GLES20.glDeleteTextures(3, yuvTextures, 0);
+ yuvTextures = null;
+ }
+ }
+}
diff --git a/Android/APIExample-Compose/app/src/main/res/drawable/ic_component.xml b/Android/APIExample-Compose/app/src/main/res/drawable/ic_component.xml
new file mode 100644
index 000000000..84894df75
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/drawable/ic_component.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_background.xml b/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_foreground.xml b/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/drawable/ic_palette_24dp.xml b/Android/APIExample-Compose/app/src/main/res/drawable/ic_palette_24dp.xml
new file mode 100644
index 000000000..b9e640b65
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/drawable/ic_palette_24dp.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/Android/APIExample-Compose/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml b/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 000000000..0e3ec3f13
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,28 @@
+
+ APIExample-Compose
+ 视频互动直播
+ 音频互动直播
+ RTC实时直播
+ 视频互动直播(Token验证)
+ 旁路推流CDN
+ 音视频元数据
+ 美声与音效
+ 自定义音频采集
+ 自定义音频渲染
+ 自定义视频采集
+ 自定义视频渲染
+ 原始音频数据
+ 原始视频数据
+ 加入多频道
+ 媒体流加密
+ 音频文件混音
+ 通话前质量检测
+ 本地/远端录制
+ 媒体播放器
+ 屏幕共享
+ 视频增强组件
+ 本地合图
+ 创建数据流
+ 虚拟节拍器
+ 跨频道媒体流转发
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/values/colors.xml b/Android/APIExample-Compose/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f8c6127d3
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/values/strings.xml b/Android/APIExample-Compose/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..31da5a3b9
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/values/strings.xml
@@ -0,0 +1,28 @@
+
+ APIExample-Compose
+ Join Video Channel
+ Join Audio Channel
+ Live Streaming
+ Join Video Channel (With Token)
+ 旁路推流CDN
+ 音视频元数据
+ 美声与音效
+ 自定义音频采集
+ 自定义音频渲染
+ 自定义视频采集
+ 自定义视频渲染
+ 原始音频数据
+ 原始视频数据
+ 加入多频道
+ 媒体流加密
+ 音频文件混音
+ 通话前质量检测
+ 本地/远端录制
+ 媒体播放器
+ 屏幕共享
+ 视频增强组件
+ 本地合图
+ 创建数据流
+ 虚拟节拍器
+ 跨频道媒体流转发
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/values/themes.xml b/Android/APIExample-Compose/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..9cd723126
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/xml/backup_rules.xml b/Android/APIExample-Compose/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/main/res/xml/data_extraction_rules.xml b/Android/APIExample-Compose/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Compose/app/src/test/java/io/agora/api/example/compose/ExampleUnitTest.kt b/Android/APIExample-Compose/app/src/test/java/io/agora/api/example/compose/ExampleUnitTest.kt
new file mode 100644
index 000000000..3022293be
--- /dev/null
+++ b/Android/APIExample-Compose/app/src/test/java/io/agora/api/example/compose/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package io.agora.api.example.compose
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/Android/APIExample-Compose/build.gradle.kts b/Android/APIExample-Compose/build.gradle.kts
new file mode 100644
index 000000000..a0985efc8
--- /dev/null
+++ b/Android/APIExample-Compose/build.gradle.kts
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.jetbrainsKotlinAndroid) apply false
+}
\ No newline at end of file
diff --git a/Android/APIExample-Compose/gradle.properties b/Android/APIExample-Compose/gradle.properties
new file mode 100644
index 000000000..20e2a0152
--- /dev/null
+++ b/Android/APIExample-Compose/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/Android/APIExample-Compose/gradle/libs.versions.toml b/Android/APIExample-Compose/gradle/libs.versions.toml
new file mode 100644
index 000000000..699c40d4f
--- /dev/null
+++ b/Android/APIExample-Compose/gradle/libs.versions.toml
@@ -0,0 +1,45 @@
+[versions]
+agp = "8.3.1"
+datastore = "1.0.0"
+kotlin = "1.9.0"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.2"
+composeBom = "2023.08.00"
+loggingInterceptor = "4.10.0"
+materialIconsExtended = "1.6.0"
+navigationCompose = "2.7.7"
+agoraSdk = "4.3.0"
+okhttp = "4.10.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
+agora-full-sdk = { module = "io.agora.rtc:full-sdk", version.ref = "agoraSdk" }
+agora-full-screen-sharing = { module = "io.agora.rtc:full-screen-sharing", version.ref = "agoraSdk" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
+okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.jar b/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e708b1c02
Binary files /dev/null and b/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.properties b/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..1d1ebbfd2
--- /dev/null
+++ b/Android/APIExample-Compose/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+#Wed Apr 10 23:59:46 CST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+#distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Android/APIExample-Compose/gradlew b/Android/APIExample-Compose/gradlew
new file mode 100755
index 000000000..4f906e0c8
--- /dev/null
+++ b/Android/APIExample-Compose/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/Android/APIExample-Compose/gradlew.bat b/Android/APIExample-Compose/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/Android/APIExample-Compose/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Android/APIExample-Compose/settings.gradle.kts b/Android/APIExample-Compose/settings.gradle.kts
new file mode 100644
index 000000000..a29b84ff8
--- /dev/null
+++ b/Android/APIExample-Compose/settings.gradle.kts
@@ -0,0 +1,25 @@
+pluginManagement {
+ repositories {
+ maven { url = uri("https://maven.aliyun.com/repository/public") }
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ maven { url = uri("https://maven.aliyun.com/repository/public") }
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "APIExample-Compose"
+include(":app")