diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6ed70cdafff039bfedba294c781be865ca5f6c1e..839ab18b2845cf6f14f778d82f3ebdb68fe37fd0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,15 @@
package="com.huawei.cloudapp">
+
+
+
+
+
+
0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ this.sendBroadcast(new Intent(GRANT_CAMERA_PERMISSION_SUCCESS_ACTION));
+ }
+ } else if (requestCode == DEV_TYPE_MICROPHONE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ this.sendBroadcast(new Intent(GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION));
+ }
+ }
+ }
+
/**
* get data with intent
*/
@@ -383,6 +419,13 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl
}
}
+ private void initVirtualDeviceSession() {
+ if (!isGotCameraAndRecordPermission) return;
+ mVirtualDeviceSession = new VirtualDeviceSession(this);
+ mRingBufferVirtualDeviceIO = new RingBufferVirtualDeviceIO();
+ mVirtualDeviceSession.setVirtualDeviceIoHook(mRingBufferVirtualDeviceIO);
+ }
+
private void processStateChangeMsg(int code) {
CASLog.i(TAG, "processStateChangeMsg code = " + code);
switch (code) {
@@ -390,6 +433,9 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl
lag.setVisibility(View.VISIBLE);
loadingView.setVisibility(View.GONE);
startSuccessThreadTask();
+ if (mVirtualDeviceSession != null) {
+ mVirtualDeviceSession.start();
+ }
break;
case CasState.CAS_SERVER_UNREACHABLE:
showDialog(getResources().getString(R.string.cas_phone_connect_server_fail_tip_message));
@@ -603,6 +649,9 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl
bIsStart = false;
try {
mCloudPhone.exitCloudPhone();
+ if (mVirtualDeviceSession != null) {
+ mVirtualDeviceSession.stop();
+ }
} catch (Exception e) {
CASLog.e(TAG, "stop cloud phone failed " + e.getMessage());
}
@@ -781,6 +830,16 @@ public class CasCloudPhoneActivity extends FragmentActivity implements View.OnCl
}
}
+ class CloudPhoneVirtualDevDataListenerImpl implements CloudPhoneVirtualDevDataListener {
+
+ @Override
+ public void onRecvVirtualDevData(byte[] data, int length) {
+ if (mRingBufferVirtualDeviceIO != null) {
+ mRingBufferVirtualDeviceIO.fillData(data);
+ }
+ }
+ }
+
public void switchCtrlViewShow() {
if (ctrView == null) {
return;
diff --git a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl
index bafce7ea95280ef05f3a4281790a8facbfd70533..561f92e280aef55202820acee091f9aecd7d6c94 100644
--- a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl
+++ b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlInterface.aidl
@@ -72,5 +72,5 @@ interface ICASAidlInterface {
void mute(in boolean isMute);
- void sendDataToCloudApp(in byte[] data);
+ void sendData(byte type, in byte[] data);
}
diff --git a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl
index f4e146b99a8afbdb3e9905222163cd784da098b9..c3a9fee9fd9cd60f08b207ac4e63c48fd09ab62a 100644
--- a/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl
+++ b/cloudphone/src/main/aidl/com/huawei/cloudphone/ICASAidlListener.aidl
@@ -26,4 +26,6 @@ interface ICASAidlListener {
void onCmdRecv(int code, String msg);
void onChannelDataRecv(in byte[] data);
+
+ void onVirtualDevDataRecv(in byte[] data);
}
diff --git a/cloudphone/src/main/cpp/CasCommon.h b/cloudphone/src/main/cpp/CasCommon.h
index 6b958c7a68adb664013e021d771c0ce15bcf5d16..01d70e112ddef7348292fc8751e7cad998d3e78d 100644
--- a/cloudphone/src/main/cpp/CasCommon.h
+++ b/cloudphone/src/main/cpp/CasCommon.h
@@ -39,6 +39,12 @@ const std::string KEY_BITRATE = "bitrate";
const std::string KEY_VIRTUAL_WIDTH = "virtual_width";
const std::string KEY_VIRTUAL_HEIGHT = "virtual_height";
+enum VirtualDevice {
+ VIRTUAL_CAMERA,
+ VIRTUAL_MICROPHONE,
+ VIRTUAL_SENSOR
+};
+
enum {
CAS_CONNECTING = 0x0100,
CAS_CONNECT_SUCCESS = 0x0200,
diff --git a/cloudphone/src/main/cpp/CasController.cpp b/cloudphone/src/main/cpp/CasController.cpp
index 08ebabf761dd7ca9a79b218a4b54ecabac93e072..0b46d5944b0ea503860530619900ccf9f72013ec 100644
--- a/cloudphone/src/main/cpp/CasController.cpp
+++ b/cloudphone/src/main/cpp/CasController.cpp
@@ -65,6 +65,7 @@ CasController::CasController()
m_videoPacketStream = nullptr;
m_audioPacketStream = nullptr;
m_orientationStream = nullptr;
+ m_virtualDeviceStream = nullptr;
m_controlStream = nullptr;
m_channelStream = nullptr;
m_cmdController = nullptr;
@@ -85,6 +86,7 @@ CasController::~CasController()
m_videoPacketStream = nullptr;
m_audioPacketStream = nullptr;
m_orientationStream = nullptr;
+ m_virtualDeviceStream = nullptr;
m_controlStream = nullptr;
m_channelStream = nullptr;
m_cmdController = nullptr;
@@ -398,6 +400,7 @@ bool CasController::CreateWorkers(ANativeWindow *nativeWindow, bool needVideoDec
m_streamParser->SetServiceHandle(CasMsgType::Video, m_videoPacketStream);
m_streamParser->SetServiceHandle(CasMsgType::Audio, m_audioPacketStream);
m_streamParser->SetServiceHandle(CasMsgType::Channel, m_channelStream);
+ m_streamParser->SetServiceHandle(CasMsgType::VirtualDevice, m_virtualDeviceStream);
if (needVideoDecode) {
m_videoDecodeThread = new (std::nothrow) CasVideoHDecodeThread(nativeWindow);
@@ -593,6 +596,12 @@ bool CasController::InitDataStream()
return false;
}
+ m_virtualDeviceStream = new (std::nothrow) CasDataPipe();
+ if (m_virtualDeviceStream == nullptr) {
+ ERR("Failed to new virtual device packet stream.");
+ return false;
+ }
+
return true;
}
@@ -618,6 +627,10 @@ bool CasController::ClearDataStream()
m_channelStream->Clear();
}
+ if (m_virtualDeviceStream != nullptr) {
+ m_virtualDeviceStream->Clear();
+ }
+
INFO("Succeed to clear data stream ");
return true;
}
@@ -745,6 +758,11 @@ int CasController::JniRecvData(CasMsgType type, uint8_t *data, int length)
pPkt = m_channelStream->GetNextPkt();
}
break;
+ case CasMsgType::VirtualDevice:
+ if (m_virtualDeviceStream != nullptr) {
+ pPkt = m_virtualDeviceStream->GetNextPkt();
+ }
+ break;
default:
ERR("Invalid type %d, length %d.", type, length);
return 0;
diff --git a/cloudphone/src/main/cpp/CasController.h b/cloudphone/src/main/cpp/CasController.h
index 2ef53ca96a575bac88cbdf8c4a9fdb6affb956bc..f7d61dba07abdda5ef9af1fd1cdd72e52c62e225 100644
--- a/cloudphone/src/main/cpp/CasController.h
+++ b/cloudphone/src/main/cpp/CasController.h
@@ -125,6 +125,7 @@ private:
CasDataPipe *m_orientationStream = nullptr;
CasDataPipe *m_controlStream = nullptr;
CasDataPipe *m_channelStream = nullptr;
+ CasDataPipe *m_virtualDeviceStream = nullptr;
CasCmdController *m_cmdController = nullptr;
CasHeartbeatController *m_heartbeatController = nullptr;
diff --git a/cloudphone/src/main/cpp/CasOpusBridge.cpp b/cloudphone/src/main/cpp/CasOpusBridge.cpp
index 001f56ea001b1e986b5f823dc7244621385f5aeb..1f6f840777b562477f7586a5fa2835cf73f0ad57 100644
--- a/cloudphone/src/main/cpp/CasOpusBridge.cpp
+++ b/cloudphone/src/main/cpp/CasOpusBridge.cpp
@@ -97,7 +97,7 @@ extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIW
}
extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusEncoder(JNIEnv *env,
- jclass type, jint sampleRate, jint channels, jint bitrate)
+ jclass type, jint sampleRate, jint channels)
{
int err = 0;
g_encoder = opus_encoder_create(sampleRate, channels, APPLICATION, &err);
@@ -106,8 +106,9 @@ extern "C" JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIW
g_encoder = nullptr;
return JNI_ERR;
}
-
- err = opus_encoder_ctl(g_encoder, OPUS_SET_BITRATE(bitrate));
+ err += opus_encoder_ctl(g_encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH_WIDEBAND));
+ err += opus_encoder_ctl(g_encoder, OPUS_SET_VBR(1));
+ err += opus_encoder_ctl(g_encoder, OPUS_SET_COMPLEXITY(10));
if (err < 0) {
ERR("Failed to set bitrate %s", opus_strerror(err));
g_encoder = nullptr;
diff --git a/cloudphone/src/main/cpp/CasOpusBridge.h b/cloudphone/src/main/cpp/CasOpusBridge.h
index db67993cf15fc489db68a004e95db14065bb104c..94f1dcafb5e7298262ef8481faf751c386dd84a1 100644
--- a/cloudphone/src/main/cpp/CasOpusBridge.h
+++ b/cloudphone/src/main/cpp/CasOpusBridge.h
@@ -58,7 +58,7 @@ JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_opus
* Signature: (III)I
*/
JNIEXPORT jint JNICALL Java_com_huawei_cloudphone_jniwrapper_OpusJNIWrapper_createOpusEncoder(JNIEnv *, jclass type,
- jint, jint, jint);
+ jint, jint);
/*
* Class: com_huawei_cloudphone_jniwrapper_OpusJNIWrapper
diff --git a/cloudphone/src/main/cpp/cas_common/CasMsg.h b/cloudphone/src/main/cpp/cas_common/CasMsg.h
index 8a44a05ef95c3247a9c13428e72bc22ddce36e21..6414caf0d9f07f37a5ad8224bd4e706d4430cc5b 100644
--- a/cloudphone/src/main/cpp/cas_common/CasMsg.h
+++ b/cloudphone/src/main/cpp/cas_common/CasMsg.h
@@ -33,6 +33,10 @@ enum CasMsgType : uint8_t {
Recorder = 11,
KeyEventInput = 15,
MotionEventInput = 16,
+ VirtualDevice = 20,
+ VirtualCamera = 21,
+ VirtualMicrophone = 22,
+ VirtualSensor = 23,
End,
};
@@ -52,6 +56,9 @@ enum CasMsgType : uint8_t {
#define CAS_MSG_CHECKSUM_KEYEVENTINPUT GET_CAS_CHECKSUM(CasMsgType::KeyEventInput)
#define CAS_MSG_CHECKSUM_MOTIONEVENTINPUT GET_CAS_CHECKSUM(CasMsgType::MotionEventInput)
#define CAS_MSG_CHECKSUM_CHANNEL GET_CAS_CHECKSUM(CasMsgType::Channel)
+#define CAS_MSG_CHECKSUM_VIRTUAL_CAMERA GET_CAS_CHECKSUM(CasMsgType::VirtualCamera)
+#define CAS_MSG_CHECKSUM_VIRTUAL_MICROPHONE GET_CAS_CHECKSUM(CasMsgType::VirtualMicrophone)
+#define CAS_MSG_CHECKSUM_VIRTUAL_SENSOR GET_CAS_CHECKSUM(CasMsgType::VirtualSensor)
// 客户端通用消息头
typedef struct streamMsgHead {
diff --git a/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp b/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp
index 78b9cc20241e7cef35892b05eb8a43269180e954..12576549c179d3c4516fd0ff9a2e54938aa51692 100644
--- a/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp
+++ b/cloudphone/src/main/cpp/cas_stream/CasStreamBuildSender.cpp
@@ -72,6 +72,15 @@ int CasStreamBuildSender::SendDataToServer(CasMsgType type, const void *buf, siz
case (Channel):
msgHead.checksum = CAS_MSG_CHECKSUM_CHANNEL;
break;
+ case (VirtualCamera):
+ msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_CAMERA;
+ break;
+ case (VirtualMicrophone):
+ msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_MICROPHONE;
+ break;
+ case (VirtualSensor):
+ msgHead.checksum = CAS_MSG_CHECKSUM_VIRTUAL_SENSOR;
+ break;
default: {
return -1;
}
diff --git a/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp b/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp
index 6197e385529649277ad0cf25528c5be50fd36c7a..60c1257cef0d7331ff5601900387e9f2673c5d21 100644
--- a/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp
+++ b/cloudphone/src/main/cpp/cas_stream/CasStreamRecvParser.cpp
@@ -75,7 +75,7 @@ void CasStreamRecvParser::SetServiceHandle(unsigned char type, CasPktHandle *ser
CasPktHandle *CasStreamRecvParser::GetServiceHandle(unsigned char type)
{
- return m_serviceHandles[type];
+ return VirtualSensor >= type && type >= VirtualCamera ? m_serviceHandles[VirtualDevice] : m_serviceHandles[type];
}
CasStreamParseThread::CasStreamParseThread(CasSocket *socket, CasStreamRecvParser *streamRecvParser)
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java b/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..abee14c94b755883afa7165971bd03baf063e63c
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/api/CloudPhoneVirtualDevDataListener.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.api;
+
+public interface CloudPhoneVirtualDevDataListener {
+ void onRecvVirtualDevData(byte[] data, int length);
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java b/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java
index 0311b01f72ef99490e84d0d7ee42579cbe5f7ee4..5cf371a09112b38286d00dd036e1a0b8f43165b5 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/api/ICloudPhone.java
@@ -115,6 +115,20 @@ public interface ICloudPhone {
*/
void registerOnOrientationChangeListener(CloudPhoneOrientationChangeListener listener);
+ /**
+ * 设置虚拟设备的回调
+ *
+ * @param listener
+ */
+ void registerOnVirtualDevDataListener(CloudPhoneVirtualDevDataListener listener);
+
+ /**
+ * 发送虚拟设备数据
+ * @param devType
+ * @param data
+ */
+ void sendVirtualDeviceData(byte devType, byte[] data);
+
/**
* 获取网络时延
*
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java b/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java
index 1d0bf5b0a6b4770433864ef11f1212fd09ce728b..ca6d21cf730abd55f3c83ee90f435d5d9abc7bf3 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/apiimpl/CloudPhoneImpl.java
@@ -47,6 +47,7 @@ import com.huawei.cloudphone.api.CloudPhoneOrientationChangeListener;
import com.huawei.cloudphone.api.CloudPhoneParas;
import com.huawei.cloudphone.api.CloudPhoneParas.DisplayMode;
import com.huawei.cloudphone.api.CloudPhoneStateListener;
+import com.huawei.cloudphone.api.CloudPhoneVirtualDevDataListener;
import com.huawei.cloudphone.api.ICloudPhone;
import com.huawei.cloudphone.common.CASLog;
import com.huawei.cloudphone.common.CasConnectorInfo;
@@ -113,6 +114,7 @@ public class CloudPhoneImpl implements ICloudPhone {
private volatile boolean mIsReconnectTaskRun = false;
private CloudPhoneStateListener mStateListener = null;
private CloudPhoneOrientationChangeListener mOrientationChangeListener = null;
+ private CloudPhoneVirtualDevDataListener mVirtualDevDataListener = null;
private CloudAppDataListener mChannelDataListener = null;
private ServiceConnection mConnection = null;
private CASListener mListener = null;
@@ -295,6 +297,18 @@ public class CloudPhoneImpl implements ICloudPhone {
mOrientationChangeListener = listener;
}
+ @Override
+ public void registerOnVirtualDevDataListener(CloudPhoneVirtualDevDataListener listener) {
+ mVirtualDevDataListener = listener;
+ }
+
+ @Override
+ public void sendVirtualDeviceData(byte devType, byte[] data) {
+ if (mCASClient != null) {
+ mCASClient.sendDataToVirtualDevice(devType, data);
+ }
+ }
+
@Override
public int getRtt() {
if (mCASClient != null) {
@@ -495,7 +509,7 @@ public class CloudPhoneImpl implements ICloudPhone {
mCASClient.pause();
mCurrentState = STATE_PAUSE;
mBackGroundTimer = new Timer();
- mBackGroundTimer.schedule(new BackgroudTimerTask(), mBgTimeout);
+ mBackGroundTimer.schedule(new BackgroundTimerTask(), mBgTimeout);
}
}
}
@@ -767,9 +781,16 @@ public class CloudPhoneImpl implements ICloudPhone {
mChannelDataListener.onRecvCloudAppData(data);
}
}
+
+ @Override
+ public void onVirtualDevDataRecv(byte[] data) throws RemoteException {
+ if (mVirtualDevDataListener != null) {
+ mVirtualDevDataListener.onRecvVirtualDevData(data, data.length);
+ }
+ }
}
- private class BackgroudTimerTask extends TimerTask {
+ private class BackgroundTimerTask extends TimerTask {
@Override
public void run() {
CASLog.i(TAG, "BackgroudTimerTask run");
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java
index 95421afd6d3038b8fdafaafa81fe72dae4ab04d7..f9d626541d1de10350b33a0298550c198191aea2 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/JNIWrapper.java
@@ -51,6 +51,11 @@ public class JNIWrapper {
public static final byte NOTIFY = 17;
public static final byte END = 19;
+ public static final byte VIRTUAL_DEVICE_DATA = 20;
+ public static final byte CAMERA_DATA = 21;
+ public static final byte MICROPHONE_DATA = 22;
+ public static final byte SENSOR_DATA = 23;
+
static {
System.loadLibrary("cloudapp");
}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java
index 2871dbb310fc0792be0c3f8cac7cd0a0241708e6..a5dd9340199601eb1dc2746addbb9747f5dc26f3 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/jniwrapper/OpusJNIWrapper.java
@@ -29,7 +29,7 @@ public class OpusJNIWrapper {
public static native int opusDecode(long decoder, byte[] inputBuffer, int inputBufferLen, short[] outBuffer, int outbufLen);
- public static native int createOpusEncoder(int sampleRate, int channels, int bitrate);
+ public static native int createOpusEncoder(int sampleRate, int channels);
public static native int destroyOpusEncoder();
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java b/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java
index 1f46aabf2ac1b744340d9429db539e116703c2d3..274b22c77ef71e53ae8f8071ae70feed48d52169 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/service/CASClient.java
@@ -16,6 +16,8 @@
package com.huawei.cloudphone.service;
+import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CHANNEL;
+
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Surface;
@@ -321,9 +323,20 @@ public class CASClient {
return;
}
try {
- mCasInterface.sendDataToCloudApp(data);
+ mCasInterface.sendData(CHANNEL, data);
} catch (RemoteException e) {
CASLog.e(TAG, "failed to send data to cloud app.");
}
}
+
+ public void sendDataToVirtualDevice(byte devType, byte[] data) {
+ if (mCasInterface == null) {
+ return;
+ }
+ try {
+ mCasInterface.sendData(devType, data);
+ } catch (RemoteException e) {
+ CASLog.e(TAG, "failed to send data to virtual device.");
+ }
+ }
}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java b/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java
index 0d0eb0a5497006d7cbf059aa376699094f8fef41..cf1682225eb3c82fed1e80e05393af37cd2163b7 100644
--- a/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/service/CasProcessor.java
@@ -40,15 +40,11 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
-import static com.huawei.cloudphone.jniwrapper.JNIWrapper.AUDIO;
-import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CHANNEL;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_AES_IV;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_AUTH_TS;
-import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_DECODE_METHOD;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_ENCRYPTED_DATA;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_BACKGROUND_TIMEOUT;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_IP;
-import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_LOG_LEVEL;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_PORT;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_PROTOCOL_VERSION;
import static com.huawei.cloudphone.jniwrapper.JNIWrapper.KEY_SDK_VERSION;
@@ -91,6 +87,7 @@ public class CasProcessor extends ICASAidlInterface.Stub {
*/
private ICASAidlListener mListener = null;
private NewRotationDirectionPacket mNewRotationDirPkt;
+ private NewVirtualDevDataPacket mVirtualDevDataPkt;
@Override
public void init() throws RemoteException {
@@ -218,10 +215,12 @@ public class CasProcessor extends ICASAidlInterface.Stub {
mAudioTrackerCallback = new AudioTrackerCallback();
mNewRotationDirPkt = new NewRotationDirectionPacket();
mChannelDataCallback = new NewChannelDataPacket();
+ mVirtualDevDataPkt = new NewVirtualDevDataPacket();
mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.ORIENTATION, mNewRotationDirPkt);
mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.AUDIO, mAudioTrackerCallback);
mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.CHANNEL, mChannelDataCallback);
+ mUpstreamReceiveDispatcher.addNewPacketCallback((byte) JNIWrapper.VIRTUAL_DEVICE_DATA, mVirtualDevDataPkt);
mUpstreamReceiveDispatcher.start();
return true;
}
@@ -232,6 +231,7 @@ public class CasProcessor extends ICASAidlInterface.Stub {
mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.AUDIO);
mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.ORIENTATION);
mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.CHANNEL);
+ mUpstreamReceiveDispatcher.deleteNewPacketCallback((byte) JNIWrapper.VIRTUAL_DEVICE_DATA);
mUpstreamReceiveDispatcher.stopBlocked();
mAudioTrackerCallback.closeAudioTrack();
mUpstreamReceiveDispatcher = null;
@@ -268,8 +268,8 @@ public class CasProcessor extends ICASAidlInterface.Stub {
}
@Override
- public void sendDataToCloudApp(byte[] data) throws RemoteException {
- JniBridge.getInstance().sendData(CHANNEL, data, data.length);
+ public void sendData(byte devType, byte[] data) throws RemoteException {
+ JniBridge.getInstance().sendData(devType, data, data.length);
}
/**
@@ -340,4 +340,17 @@ public class CasProcessor extends ICASAidlInterface.Stub {
}
}
}
+
+ private class NewVirtualDevDataPacket implements NewPacketCallback {
+ @Override
+ public void onNewPacket(byte[] data) {
+ if (mListener != null) {
+ try {
+ mListener.onVirtualDevDataRecv(data);
+ } catch (RemoteException e) {
+ CASLog.e(TAG, "call onVirtualDevDataRecv failed." + e.getMessage());
+ }
+ }
+ }
+ }
}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..a4ff95eaef3871ac06226f7ca0877503bde4ee2a
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/VirtualDeviceSession.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.view.SurfaceHolder;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceIO;
+import com.huawei.cloudphone.virtualdevice.common.ParamBundle;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol;
+
+public class VirtualDeviceSession {
+ VirtualDeviceProtocol mVirtualDevice;
+ Context mContext;
+ ParamBundle mParamBundle;
+ IVirtualDeviceIO mVirtualDeviceIO;
+
+ public VirtualDeviceSession(Context context) {
+ mParamBundle = new ParamBundle();
+ mParamBundle.setAppContext(context);
+ mContext = context;
+ }
+
+ public void setCameraSurfaceHolder(SurfaceHolder surfaceHolder) {
+ mParamBundle.setSurfaceHolder(surfaceHolder);
+ }
+
+ public void setVirtualDeviceIoHook(IVirtualDeviceIO virtualDeviceIO) {
+ mVirtualDeviceIO = virtualDeviceIO;
+ }
+
+ public void start() {
+ mVirtualDevice = new VirtualDeviceProtocol(mVirtualDeviceIO, mContext);
+ mVirtualDevice.startProcess();
+ }
+
+ public void stop() {
+ if (mVirtualDevice != null) {
+ mVirtualDevice.stopProcess();
+ mVirtualDevice = null;
+ }
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..f488194d5c69d5b06d62718c9bdaada352a2d159
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoder.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.camera;
+
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
+import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
+
+import android.graphics.ImageFormat;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class AvcEncoder {
+ private static final String TAG = "AvcEncoder";
+ private static final String MIME_TYPE = "video/avc";
+ private static final int TIMEOUT_USC = 1000;
+ private static final int MICROSECONDS_PER_SECOND = 1000000;
+ private static final int I_FRAME_INTERVAL = 15;
+
+ private static int colorFormat = COLOR_FormatYUV420Planar;
+ private MediaCodec mediaCodec;
+ private int mWidth;
+ private int mHeight;
+ private int mFrameRate;
+ private long genIndex;
+ private byte[] configByte;
+ private AvcEncoderDataHandler dataHandler;
+
+ public void getIFrame() {
+ mediaCodec.flush();
+ }
+
+ public AvcEncoder(int width, int height, int frameRate, int bitrate, AvcEncoderDataHandler dataHandler) {
+ Log.i(TAG, "Create AvcEncoder, width = " + width + ", height = " + height +
+ ", frameRate = " + frameRate + ", bitrate = " + bitrate);
+ this.dataHandler = dataHandler;
+ this.mWidth = width;
+ this.mHeight = height;
+ this.mFrameRate = frameRate;
+ this.genIndex = 0;
+ MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
+ mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
+ mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+ mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
+ mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL);
+ mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
+
+ try {
+ mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
+ } catch (IOException e) {
+ Log.e(TAG, "AvcEncoder create encoder by type failed. ", e);
+ }
+ mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ mediaCodec.start();
+ }
+
+ public void stopEncoder() {
+ Log.i(TAG, "Stop encoder.");
+ try {
+ mediaCodec.stop();
+ mediaCodec.release();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "AvcEncoder stop Encoder failed. ", e);
+ }
+ }
+
+ public void offerEncoder(byte[] frame, byte[] encodeBuff) {
+ if ((frame == null) || (encodeBuff == null)) {
+ Log.e(TAG, "Offer Encoder input param invalid. ");
+ return;
+ }
+ inputBuffer(frame);
+ int outBufLen = 0;
+ while (true) {
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ int outBufId = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USC);
+ Log.i(TAG, "bufferInfo: size = " + bufferInfo.size + ", offset = " + bufferInfo.offset +
+ ", flags = " + bufferInfo.flags + ", presentationTimeUs = " + bufferInfo.presentationTimeUs);
+ if (outBufId >= 0) {
+ ByteBuffer outBuf = mediaCodec.getOutputBuffer(outBufId);
+ byte[] h264Buf = new byte[bufferInfo.size];
+ if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
+ configByte = new byte[bufferInfo.size];
+ outBuf.get(configByte);
+ } else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
+ outBuf.get(h264Buf);
+ System.arraycopy(configByte, 0, encodeBuff, 0, configByte.length);
+ System.arraycopy(h264Buf, 0, encodeBuff, configByte.length, h264Buf.length);
+ outBufLen = bufferInfo.size + configByte.length;
+ dataHandler.handleData(encodeBuff, outBufLen);
+ } else {
+ outBuf.get(h264Buf);
+ System.arraycopy(h264Buf, 0, encodeBuff, 0, h264Buf.length);
+ outBufLen = bufferInfo.size;
+ dataHandler.handleData(encodeBuff, outBufLen);
+ }
+ mediaCodec.releaseOutputBuffer(outBufId, false);
+ } else if (outBufId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ Log.e(TAG, "Output format changed : " + outBufId);
+ return;
+ } else if (outBufId == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ Log.e(TAG, "Codec waiting : " + outBufId);
+ return;
+ } else {
+ Log.e(TAG, "Unknown outBufId : " + outBufId);
+ return;
+ }
+ }
+ }
+
+ private void inputBuffer(byte[] frame) {
+ long pts = computePresentationTime(genIndex);
+ genIndex++;
+ byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
+ if (colorFormat == COLOR_FormatYUV420Planar) {
+ swapYV12ToI420(frame, yuv420sp, mWidth, mHeight);
+ }else if (colorFormat == COLOR_FormatYUV420SemiPlanar) {
+ swapNV21ToNV12(frame, yuv420sp, mWidth, mHeight);
+ }
+ int inBuffId = mediaCodec.dequeueInputBuffer(TIMEOUT_USC);
+ if (inBuffId < 0) {
+ Log.e(TAG, "Dequeue input buffer error, inBuffId = " + inBuffId);
+ return;
+ }
+ ByteBuffer inBuff = mediaCodec.getInputBuffer(inBuffId);
+ inBuff.clear();
+ inBuff.put(yuv420sp);
+ mediaCodec.queueInputBuffer(inBuffId, 0, yuv420sp.length, pts, 0);
+ }
+
+ private void swapYV12ToI420(byte[] yv12Data, byte[] i420Data, int width, int height) {
+ if (yv12Data == null || i420Data == null) {
+ return;
+ }
+ int frameSize = width * height;
+ System.arraycopy(yv12Data, 0, i420Data, 0, frameSize);
+ System.arraycopy(yv12Data, frameSize + frameSize / 4, i420Data, frameSize ,frameSize / 4);
+ System.arraycopy(yv12Data, frameSize, i420Data, frameSize + frameSize / 4, frameSize / 4);
+ }
+
+ private void swapNV21ToNV12(byte[] nv21Data, byte[] nv12Data, int width, int height) {
+ if (nv21Data == null || nv12Data == null) {
+ return;
+ }
+ int frameSize = width * height;
+ System.arraycopy(nv21Data, 0, nv12Data, 0, frameSize);
+ for (int i = 0; i < frameSize / 2; i += 2) {
+ nv12Data[frameSize + i - 1] = nv21Data[i + frameSize];
+ }
+ for (int i = 0; i < frameSize / 2; i += 2) {
+ nv12Data[frameSize + i] = nv21Data[i + frameSize - 1];
+ }
+ }
+
+ private long computePresentationTime(long frameIndex) {
+ return frameIndex * MICROSECONDS_PER_SECOND / mFrameRate;
+ }
+
+ public static int getImageFormat() {
+ return colorFormat == COLOR_FormatYUV420Planar ? ImageFormat.YV12 : ImageFormat.NV21;
+ }
+
+ public static boolean isSupportH264() {
+ MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
+ if (codecInfo == null) {
+ Log.e(TAG, "Couldn't find a codec for " + MIME_TYPE);
+ return false;
+ }
+ return selectColorFormat(codecInfo, MIME_TYPE);
+ }
+
+ private static MediaCodecInfo selectCodec(String mimeType) {
+ for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
+ MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
+ if(!codecInfo.isEncoder()) {
+ continue;
+ }
+ String[] types = codecInfo.getSupportedTypes();
+ for (int j = 0; j < types.length; j++) {
+ if (types[j].equalsIgnoreCase(mimeType)) {
+ return codecInfo;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean selectColorFormat(MediaCodecInfo codecInfo, String mimeType) {
+ MediaCodecInfo.CodecCapabilities capabilities;
+ try {
+ capabilities = codecInfo.getCapabilitiesForType(mimeType);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Couldn't get capabilities for " + MIME_TYPE);
+ return false;
+ }
+ for (int i = 0; i < capabilities.colorFormats.length; i++) {
+ int colorFormat = capabilities.colorFormats[i];
+ if (colorFormat == COLOR_FormatYUV420SemiPlanar) {
+ AvcEncoder.colorFormat = COLOR_FormatYUV420SemiPlanar;
+ return true;
+ } else if (colorFormat == COLOR_FormatYUV420Planar) {
+ AvcEncoder.colorFormat = COLOR_FormatYUV420Planar;
+ return true;
+ }
+ }
+ Log.e(TAG, "Couldn't get a color format for " + codecInfo.getName() + "/" + mimeType);
+ return false;
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..703683387bf72bba99515e25453650db59a82061
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/AvcEncoderDataHandler.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.camera;
+
+public interface AvcEncoderDataHandler {
+
+ /**
+ * 处理编码数据
+ * @param data
+ * @param length
+ */
+ void handleData(byte[] data, int length);
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java
new file mode 100644
index 0000000000000000000000000000000000000000..c033b7d11f65d1bb0a0fe32dfd1c4144e9c72c62
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCamera.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.camera;
+
+import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
+import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
+import static android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO;
+import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
+import static android.hardware.Camera.Parameters.FOCUS_MODE_INFINITY;
+import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.graphics.YuvImage;
+import android.hardware.Camera;
+import android.opengl.GLES11Ext;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+import com.huawei.cloudphone.virtualdevice.common.ParamBundle;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+public class VirtualCamera implements Camera.PreviewCallback {
+
+ private static final String TAG = "VirtualCamera";
+ private Camera.Parameters mParameters = null;
+ private int mCameraId = 0;
+ private Camera mCamera = null;
+ private static final int LOG_LENGTH_LIMIT = 2000;
+ private static final int SIZE_WIDTH_LIMIT = 1280;
+ private static final int JPEG_BUFFER_SIZE = 8 * 1024 * 1024;
+ private static final int DEFAULT_WIDTH = 640;
+ private static final int DEFAULT_HEIGHT = 480;
+
+ private int mWidth = DEFAULT_WIDTH;
+ private int mHeight = DEFAULT_HEIGHT;
+ private int mFps = 30;
+ private int mBitrate = 4000000;
+ private SurfaceHolder mSurfaceHolder;
+ private boolean mIsUseH264;
+
+ private ByteArrayOutputStream jpegBuffer = new ByteArrayOutputStream(JPEG_BUFFER_SIZE);
+ private Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+ private IVirtualDeviceDataListener mCameraListener = null;
+ private byte[] mPreviewBuffer = null;
+ private SurfaceTexture mSurfaceTexture = null;
+ private byte[] mH264Buffer;
+ private AvcEncoder mAvcCodec = null;
+
+ public int open(int cameraId) {
+ mPreviewBuffer = null;
+ mSurfaceTexture = null;
+ mCameraId = cameraId;
+ Camera.getCameraInfo(mCameraId, cameraInfo);
+ try {
+ if (mCamera != null) {
+ mCamera.stopPreview();
+ mCamera.setPreviewCallback(null);
+ mCamera.release();
+ mCamera = null;
+ }
+ Camera camera = Camera.open(mCameraId);
+ Camera.Parameters param = camera.getParameters();
+ mParameters = setDefaultParameters(param);
+ camera.release();
+ mIsUseH264 = AvcEncoder.isSupportH264();
+ } catch (Exception e) {
+ Log.e(TAG, "open camera failed, ", e);
+ return -1;
+ }
+ return 0;
+ }
+
+ public int startPreview() {
+ mCamera = Camera.open(mCameraId);
+ Camera.Parameters param = mCamera.getParameters();
+ if (mIsUseH264) {
+ param.setPreviewFormat(AvcEncoder.getImageFormat());
+ } else {
+ param.setPreviewFormat(ImageFormat.NV21);
+ }
+
+ Camera.Size size = param.getPreviewSize();
+ if (mWidth <= 0 || mHeight <= 0 || mFps <= 0) {
+ param.setPreviewSize(size.width, size.height);
+ } else {
+ param.setPreviewSize(mWidth, mHeight);
+ }
+
+ mCamera.setParameters(param);
+ List mFpsRangeList = new LinkedList();
+ mFpsRangeList = param.getSupportedPreviewFpsRange();
+ for (int[] ele : mFpsRangeList) {
+ Log.i(TAG, "startPreview: support fps range is " + ele[0] + "-" + ele[1]);
+ if ((mFps * 1000 >= ele[0]) && (mFps * 1000 >= ele[1])) {
+ param.setPreviewFpsRange(ele[0], ele[1]);
+ }
+ }
+
+ try {
+ mSurfaceHolder = ParamBundle.getSurfaceHolder();
+ if (mSurfaceHolder != null) {
+ mCamera.setPreviewCallback(this);
+ mCamera.setPreviewDisplay(mSurfaceHolder);
+ mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ } else {
+ mSurfaceTexture = new SurfaceTexture(GLES11Ext.GL_TEXTURE_BINDING_EXTERNAL_OES);
+ mCamera.setPreviewTexture(mSurfaceTexture);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "startPreview: failed to set preview params", e);
+ return -1;
+ }
+
+ int buffSize = mWidth * mHeight * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8;
+ mPreviewBuffer = new byte[buffSize];
+ mCamera.addCallbackBuffer(mPreviewBuffer);
+ mCamera.setPreviewCallbackWithBuffer(this);
+
+ if (mIsUseH264) {
+ mAvcCodec = new AvcEncoder(mWidth, mHeight, param.getPreviewFrameRate(), mBitrate
+ , new H264DataHandler());
+ mH264Buffer = new byte[mWidth * mHeight * 3 / 2];
+ mAvcCodec.getIFrame();
+ }
+ mCamera.startPreview();
+ return 0;
+ }
+
+ public void stopPreview() {
+ if (mCamera != null) {
+ mCamera.stopPreview();
+ mCamera.setPreviewCallback(null);
+ mCamera.release();
+ mCamera = null;
+ }
+ if (mAvcCodec != null) {
+ mAvcCodec.stopEncoder();
+ mAvcCodec = null;
+ }
+ }
+
+ public void setOnRecvData(IVirtualDeviceDataListener listener) {
+ mCameraListener = listener;
+ }
+
+ public void setResolution(int width, int height) {
+ mWidth = width == -1 ? DEFAULT_WIDTH : width;
+ mHeight = height == -1 ? DEFAULT_HEIGHT : height;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getFacing() {
+ return cameraInfo.facing;
+ }
+
+ public int getOrientation() {
+ return cameraInfo.orientation;
+ }
+
+ public byte[] getParameters() {
+ return mParameters.flatten().getBytes();
+ }
+
+ public void setParameters(byte[] data) {
+ if (mParameters == null) {
+ return;
+ }
+ mParameters.unflatten(new String(data));
+ }
+
+ public void setFps(int fps) {
+ mFps = fps;
+ }
+
+ public boolean isSupportH264() {
+ return AvcEncoder.isSupportH264();
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ Camera.Size size = camera.getParameters().getPreviewSize();
+ if (mIsUseH264) compressWithH264(data);
+ else compressWithMPEG(data, size.width, size.height);
+ if (mSurfaceTexture != null) {
+ camera.addCallbackBuffer(mPreviewBuffer);
+ }
+ }
+
+ private void compressWithMPEG(byte[] data, int width, int height) {
+ YuvImage image = new YuvImage(data, ImageFormat.NV21, width, height, null);
+ jpegBuffer.reset();
+ int quality = 50;
+ if (!image.compressToJpeg(new Rect(0, 0, width, height), quality, jpegBuffer)) {
+ Log.e(TAG, "compressWithMPEG: failed to compress to jpeg");
+ return;
+ }
+ if (mCameraListener != null) {
+ byte[] jpegData = jpegBuffer.toByteArray();
+ mCameraListener.onRecvData(jpegData, jpegData.length, mCameraId);
+ }
+ }
+
+ private void compressWithH264(byte[] data) {
+ mAvcCodec.offerEncoder(data, mH264Buffer);
+ }
+
+ private Camera.Parameters setDefaultParameters(Camera.Parameters param) {
+ Set supportFocusModes = new HashSet<>(Arrays.asList(FOCUS_MODE_AUTO
+ , FOCUS_MODE_CONTINUOUS_PICTURE, FOCUS_MODE_CONTINUOUS_VIDEO, FOCUS_MODE_FIXED
+ , FOCUS_MODE_INFINITY, FOCUS_MODE_MACRO));
+ List focusModes = param.getSupportedFocusModes();
+
+ StringBuilder sb = new StringBuilder();
+ for (String str : focusModes) {
+ if (supportFocusModes.contains(str)) {
+ sb.append(str + ",");
+ }
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ param.set("focus-mode-values", sb.toString());
+
+ List picSize = param.getSupportedPictureSizes();
+ sb = new StringBuilder();
+ for (Camera.Size size : picSize) {
+ if (size.width < SIZE_WIDTH_LIMIT) {
+ sb.append(size.width + "x" + size.height + ",");
+ }
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ param.set("picture-size-values", sb.toString());
+
+ List previewSize = param.getSupportedPreviewSizes();
+ sb = new StringBuilder();
+ for (Camera.Size size : previewSize) {
+ if (size.width < SIZE_WIDTH_LIMIT) {
+ sb.append(size.width + "x" + size.height + ",");
+ }
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ param.set("preview-size-values", sb.toString());
+
+ List videoSize = param.getSupportedVideoSizes();
+ sb = new StringBuilder();
+ for (Camera.Size size : videoSize) {
+ if (size.width < SIZE_WIDTH_LIMIT) {
+ sb.append(size.width + "x" + size.height + ",");
+ }
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ param.set("video-size-values", sb.toString());
+ return param;
+ }
+
+ class H264DataHandler implements AvcEncoderDataHandler {
+ @Override
+ public void handleData(byte[] data, int length) {
+ if (mCameraListener != null) mCameraListener.onRecvData(data, length, mCameraId);
+ }
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..00317b5044b59b2b519185b92d5c5eaeb2b21aef
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/camera/VirtualCameraManager.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.camera;
+
+import static com.huawei.cloudphone.jniwrapper.JNIWrapper.CAMERA_DATA;
+import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader;
+
+public class VirtualCameraManager extends VirtualDeviceManager {
+ private static final String TAG = "VirtualCameraManager";
+
+ private static final short OPT_CAMERA_GET_PARAM_REQ = 0x1;
+ private static final short OPT_CAMERA_GET_PARAM_RSP = 0x1001;
+ private static final short OPT_CAMERA_SET_PARAM_REQ = 0x2;
+ private static final short OPT_CAMERA_SET_PARAM_RSP = 0x1002;
+ private static final short OPT_CAMERA_GET_INFO_REQ = 0x3;
+ private static final short OPT_CAMERA_GET_INFO_RSP = 0x1003;
+ private static final short OPT_CAMERA_START_PREVIEW_REQ = 0x4;
+ private static final short OPT_CAMERA_START_PREVIEW_RSP = 0x1004;
+ private static final short OPT_CAMERA_STOP_PREVIEW_REQ = 0x5;
+ private static final short OPT_CAMERA_STOP_PREVIEW_RSP = 0x1005;
+ private static final short OPT_CAMERA_IS_SUPPORT_H264_REQ = 0x6;
+ private static final short OPT_CAMERA_IS_SUPPORT_H264_RSP = 0x1006;
+ private static final short OPT_CAMERA_GET_SIZE_REQ = 0x7;
+ private static final short OPT_CAMERA_GET_SIZE_RSP = 0x1007;
+ private static final short OPT_CAMERA_FRAME = 0x8;
+ private static final short OPT_CAMERA_INVALID = 0x9;
+
+ private static final int RSP_RESULT_LENGTH = 2;
+ private static final int RSP_INFO_LENGTH = 3;
+ private static final int RSP_SIZE_LENGTH = 4;
+ private static final int RSP_SUPPORT_H264_LENGTH = 1;
+ public static final String GRANT_CAMERA_PERMISSION_SUCCESS_ACTION = "android.intent.action.GRANT_CAMERA_PERMISSION_SUCCESS";
+
+ private VirtualCamera mVirtualCamera;
+ private VirtualDeviceProtocol mVirtualDeviceProtocol;
+ private Context mContext;
+ private int mWidth;
+ private int mHeight;
+ private int mFrameRate;
+ private int mDevId;
+
+ public VirtualCameraManager(VirtualDeviceProtocol virtualDeviceProtocol, Context context) {
+ mVirtualCamera = new VirtualCamera();
+ mVirtualDeviceProtocol = virtualDeviceProtocol;
+ mContext = context;
+ }
+
+ public void stop() {
+ mVirtualCamera.stopPreview();
+ }
+
+ public void processMsg(MsgHeader header, byte[] body) {
+ switch (header.mOptType) {
+ case OPT_CAMERA_GET_PARAM_REQ:
+ Log.i(TAG, "processMsg: get param.");
+ handleGetParamReq(header, body);
+ break;
+ case OPT_CAMERA_SET_PARAM_REQ:
+ Log.i(TAG, "processMsg: set param.");
+ handleSetParamReq(header, body);
+ break;
+ case OPT_CAMERA_GET_INFO_REQ:
+ Log.i(TAG, "processMsg: get info.");
+ handleGetInfoReq(header, body);
+ break;
+ case OPT_CAMERA_START_PREVIEW_REQ:
+ Log.i(TAG, "processMsg: start preview.");
+ handleStartPreviewReq(header, body);
+ break;
+ case OPT_CAMERA_STOP_PREVIEW_REQ:
+ Log.i(TAG, "processMsg: stop preview.");
+ handleStopPreviewReq(header, body);
+ break;
+ case OPT_CAMERA_IS_SUPPORT_H264_REQ:
+ Log.i(TAG, "processMsg: is support h264.");
+ handleIsSupportH264Req(header, body);
+ break;
+ case OPT_CAMERA_GET_SIZE_REQ:
+ Log.i(TAG, "processMsg: get size.");
+ handleGetSizeReq(header, body);
+ break;
+ default:
+ Log.e(TAG, "processMsg: Invalid msg type");
+ }
+ }
+
+ private void handleGetParamReq(MsgHeader header, byte[] body) {
+ int result = mVirtualCamera.open(header.mDeviceId);
+ byte[] param = mVirtualCamera.getParameters();
+ byte[] rspBody = new byte[param.length + RSP_RESULT_LENGTH];
+ rspBody[0] = 0x0;
+ rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1);
+ System.arraycopy(param, 0, rspBody, RSP_RESULT_LENGTH, param.length);
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + param.length;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_PARAM_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+ private void handleSetParamReq(MsgHeader header, byte[] body) {
+ mVirtualCamera.setParameters(body);
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH];
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH;
+ rspBody[0] = 0x0;
+ rspBody[1] = 0x0;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_SET_PARAM_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+ private void handleGetInfoReq(MsgHeader header, byte[] body) {
+ int face = mVirtualCamera.getFacing();
+ int orientation = mVirtualCamera.getOrientation();
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_INFO_LENGTH];
+ rspBody[0] = 0x0;
+ rspBody[1] = 0x0;
+ rspBody[2] = (byte) (face & 0x00FF);
+ rspBody[3] = (byte) (orientation >> 8);
+ rspBody[4] = (byte) (orientation & 0x00FF);
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + RSP_INFO_LENGTH;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_INFO_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+ public void initCamera() {
+ if (mVirtualCamera.open(mDevId) != 0) {
+ Log.e(TAG, "initCamera: Failed to open camera");
+ return;
+ }
+ mVirtualCamera.setOnRecvData(new CameraDataListener());
+ mVirtualCamera.setResolution(mWidth, mHeight);
+ mVirtualCamera.setFps(mFrameRate);
+ if (mVirtualCamera.startPreview() != 0) {
+ Log.e(TAG, "initCamera: Failed to start preview");
+ }
+ }
+
+ private void handleStartPreviewReq(MsgHeader header, byte[] body) {
+ mWidth = (short)((body[0] << 8) | (body[1] & 0x00FF));
+ mHeight = (short)((body[2] << 8) | (body[3] & 0x00FF));
+ mFrameRate = (body[4] << 8) | (body[5] & 0x00FF);
+ mDevId = header.mDeviceId;
+ Context context = mContext.getApplicationContext();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
+ if (context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions((Activity) mContext,
+ new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ DEV_TYPE_CAMERA);
+ } else {
+ initCamera();
+ }
+ }
+
+ private void handleStopPreviewReq(MsgHeader header, byte[] body) {
+ mVirtualCamera.setOnRecvData(null);
+ mVirtualCamera.stopPreview();
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH];
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_STOP_PREVIEW_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+ private void handleIsSupportH264Req(MsgHeader header, byte[] body) {
+ boolean isSupportH264 = mVirtualCamera.isSupportH264();
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_SUPPORT_H264_LENGTH];
+ rspBody[2] = (byte) ((isSupportH264 ? 1 : 0) & 0x00FF);
+ int rspMsgLen = MSG_HEADER_LEN + RSP_SUPPORT_H264_LENGTH;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_IS_SUPPORT_H264_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+ private void handleGetSizeReq(MsgHeader header, byte[] body) {
+ int width = -1;
+ int height = -1;
+
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH + RSP_SIZE_LENGTH];
+ rspBody[0] = 0x0;
+ rspBody[1] = 0x0;
+ rspBody[2] = (byte) (width >> 8);
+ rspBody[3] = (byte) (width & 0x00FF);
+ rspBody[4] = (byte) (height >> 8);
+ rspBody[5] = (byte) (height & 0x00FF);
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH + RSP_SIZE_LENGTH;
+ MsgHeader rspHeader = new MsgHeader(OPT_CAMERA_GET_SIZE_RSP, DEV_TYPE_CAMERA, header.mDeviceId, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, CAMERA_DATA);
+ }
+
+
+ class CameraDataListener implements IVirtualDeviceDataListener {
+ @Override
+ public void onRecvData(Object... args) {
+ byte[] data = (byte[]) args[0];
+ int length = (int) args[1];
+ int devId = (int) args[2];
+ int repMsgLen = length + MSG_HEADER_LEN;
+ MsgHeader header = new MsgHeader(OPT_CAMERA_FRAME, DEV_TYPE_CAMERA, (short) devId, repMsgLen);
+ byte[] repBody = new byte[length];
+ System.arraycopy(data, 0, repBody, 0, length);
+ mVirtualDeviceProtocol.sendMsg(header, repBody, CAMERA_DATA);
+ }
+ }
+
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7d0f5ef775ecbbd8ffddea257469f42a6b4814a
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceDataListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+public interface IVirtualDeviceDataListener {
+
+ /**
+ * 接受数据
+ * @param args
+ */
+ void onRecvData(Object...args);
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bb0b98013c144ad22524b34ea6b7965f21014a2
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/IVirtualDeviceIO.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+public interface IVirtualDeviceIO {
+
+ /**
+ * 读取N个字节的数据
+ * @param data
+ * @param offset
+ * @param length
+ * @return
+ */
+ int readN(byte[] data, int offset, int length);
+
+ /**
+ * 发送N个字节数据
+ * @param data
+ * @param offset
+ * @param length
+ * @param deviceType
+ * @return
+ */
+ int writeN(byte[] data, int offset, int length, int deviceType);
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java
new file mode 100644
index 0000000000000000000000000000000000000000..d244a7e16d7194b004cde2bd4e881992ad278708
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/ParamBundle.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+import android.content.Context;
+import android.view.SurfaceHolder;
+
+public class ParamBundle {
+ static private Context mContext;
+ static private SurfaceHolder mSurfaceHolder;
+
+ public static void setAppContext(Context context) {
+ mContext = context;
+ }
+
+ public static void setSurfaceHolder(SurfaceHolder surfaceHolder) {
+ mSurfaceHolder = surfaceHolder;
+ }
+
+ public static SurfaceHolder getSurfaceHolder() {
+ return mSurfaceHolder;
+ }
+
+ public static Context getAppContext() {
+ return mContext;
+ }
+
+ public static void resetParam() {
+ mContext = null;
+ mSurfaceHolder = null;
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java
new file mode 100644
index 0000000000000000000000000000000000000000..97e9e8a80be8aa3fea69688706b8c182635bbb57
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBuffer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+import java.util.Arrays;
+
+public class RingBuffer {
+ private final static int DEFAULT_SIZE = 1024;
+ private Object[] buffer;
+ private int head = 0;
+ private int tail = 0;
+ private int bufferSize;
+
+ public RingBuffer() {
+ this.bufferSize = DEFAULT_SIZE;
+ this.buffer = new Object[bufferSize];
+ }
+
+ public RingBuffer(int size) {
+ this.bufferSize = size;
+ this.buffer = new Object[bufferSize];
+ }
+
+ private Boolean isEmpty() {
+ return head == tail;
+ }
+
+ private Boolean isFull() {
+ return (tail + 1) % bufferSize == head;
+ }
+
+ public void clear() {
+ Arrays.fill(buffer, null);
+ this.head = 0;
+ this.tail = 0;
+ }
+
+ public Boolean put(Object v) {
+ if (isFull()) {
+ return false;
+ }
+ buffer[tail] = v;
+ tail = (tail + 1) % bufferSize;
+ return true;
+ }
+
+ public Object get() {
+ if (isEmpty()) {
+ return null;
+ }
+ Object result = buffer[head];
+ head = (head + 1) % bufferSize;
+ return result;
+ }
+
+ public Object[] getAll() {
+ if (isEmpty()) {
+ return new Object[0];
+ }
+ int copyTail = tail;
+ int count = head < copyTail ? copyTail - head : bufferSize - head + copyTail;
+ Object[] result = new String[count];
+ if (head < copyTail) {
+ if (copyTail - head >= 0)
+ System.arraycopy(buffer, head, result, 0, copyTail - head);
+ } else {
+ if (bufferSize - head >= 0)
+ System.arraycopy(buffer, head, result, 0, bufferSize - head);
+ if (copyTail >= 0) System.arraycopy(buffer, 0, result, bufferSize - head, copyTail);
+ }
+ head = copyTail;
+ return result;
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a8f7ed6c89cae4c4fd99214de7630ca58b70307
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/RingBufferVirtualDeviceIO.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+import com.huawei.cloudphone.api.CloudPhoneManager;
+
+import java.nio.ByteBuffer;
+
+public class RingBufferVirtualDeviceIO implements IVirtualDeviceIO{
+ private RingBuffer mRingBuffer;
+ private int mDataLen;
+ private int mDataOffset;
+ private byte[] mDataBuffer;
+ private Object mObjectLock = new Object();
+ private static final int VIRTUAL_CAMERA = 0;
+ private static final int VIRTUAL_MICROPHONE = 1;
+ private static final int VIRTUAL_SENSOR = 2;
+
+ public RingBufferVirtualDeviceIO() {
+ mDataOffset = 0;
+ mDataBuffer = null;
+ mDataLen = 0;
+ mRingBuffer = new RingBuffer();
+ }
+
+
+ @Override
+ public int readN(byte[] data, int offset, int length) {
+ int ret;
+ synchronized (mObjectLock) {
+ if (mDataOffset < mDataLen && mDataBuffer != null) {
+ int oneTimeReadLength = Math.min(length, mDataLen - mDataOffset);
+ System.arraycopy(mDataBuffer, mDataOffset, data, offset, oneTimeReadLength);
+ mDataOffset += oneTimeReadLength;
+ ret = oneTimeReadLength;
+ } else {
+ ByteBuffer byteBuffer = (ByteBuffer) mRingBuffer.get();
+ if (byteBuffer != null) {
+ mDataBuffer = new byte[byteBuffer.capacity()];
+ byteBuffer.get(mDataBuffer, 0, mDataBuffer.length);
+ mDataOffset = 0;
+ mDataLen = mDataBuffer.length;
+ }
+ ret = 0;
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public int writeN(byte[] data, int offset, int length, int deviceType) {
+ synchronized (mObjectLock) {
+ CloudPhoneManager.createCloudPhoneInstance().sendVirtualDeviceData((byte) deviceType, data);
+ }
+ return length;
+ }
+
+ public void fillData(byte[] data) {
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ mRingBuffer.put(buffer);
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..48d8d5e4e3d0013b6358d8ec194b6ee743e6c4b2
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceManager.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader;
+
+public class VirtualDeviceManager {
+ public static final short DEV_TYPE_CAMERA = 1;
+ public static final short DEV_TYPE_MICROPHONE = 2;
+ public static final short DEV_TYPE_SENSOR = 0;
+
+ public void processMsg(MsgHeader header, byte[] body) {
+ }
+
+ public void start() {
+ }
+
+ public void stop() {
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e3983ec84667e9d029245d017758110074887cb
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/common/VirtualDeviceProtocol.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.common;
+
+import static com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager.DEV_TYPE_CAMERA;
+import static com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager.GRANT_CAMERA_PERMISSION_SUCCESS_ACTION;
+import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager.DEV_TYPE_MICROPHONE;
+import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager.DEV_TYPE_SENSOR;
+import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophoneManager.GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.SensorManager;
+import android.util.Log;
+
+import com.huawei.cloudphone.virtualdevice.camera.VirtualCameraManager;
+import com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophoneManager;
+import com.huawei.cloudphone.virtualdevice.sensor.VirtualSensorManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class VirtualDeviceProtocol {
+ private static final String TAG = "VirtualDeviceProtocol";
+ public static int MSG_HEADER_LEN = 16;
+ public static int WAITING_INTERVAL = 200;
+ private IVirtualDeviceIO mVirtualDeviceIO;
+ private boolean mIsTaskRun;
+ private PacketParseThread mPktProcessThread;
+ static Object mSendMsgLock = new Object();
+ private Map virtualDeviceManagers;
+ private Context mContext;
+ private BroadcastReceiver mPermissionResultReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (GRANT_CAMERA_PERMISSION_SUCCESS_ACTION.equals(intent.getAction())) {
+ ((VirtualCameraManager)virtualDeviceManagers.get(DEV_TYPE_CAMERA)).initCamera();
+ } else if (GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION.equals(intent.getAction())) {
+ ((VirtualMicrophoneManager)virtualDeviceManagers.get(DEV_TYPE_MICROPHONE)).initMicrophone();
+ }
+ }
+ };
+
+ public VirtualDeviceProtocol(IVirtualDeviceIO virtualDeviceIO ,Context context) {
+ mVirtualDeviceIO = virtualDeviceIO;
+ mContext = context;
+ initVirtualDeviceManagers();
+ }
+
+ private void initVirtualDeviceManagers() {
+ virtualDeviceManagers = new HashMap();
+ virtualDeviceManagers.put(DEV_TYPE_CAMERA, new VirtualCameraManager(this, mContext));
+ virtualDeviceManagers.put(DEV_TYPE_MICROPHONE, new VirtualMicrophoneManager(this, mContext));
+ virtualDeviceManagers.put(DEV_TYPE_SENSOR, new VirtualSensorManager(this,
+ (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE)));
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(GRANT_CAMERA_PERMISSION_SUCCESS_ACTION);
+ intentFilter.addAction(GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION);
+ mContext.registerReceiver(mPermissionResultReceiver, intentFilter);
+ }
+
+ public void processMsg(MsgHeader header, byte[] body) {
+ VirtualDeviceManager virtualDeviceManager = virtualDeviceManagers.get(header.mDeviceType);
+ if (virtualDeviceManager == null) {
+ Log.e(TAG, "processMsg: Error msg type :" + header.mDeviceType);
+ return;
+ }
+ virtualDeviceManager.processMsg(header, body);
+ }
+
+
+ public void startProcess() {
+ mIsTaskRun = true;
+ mPktProcessThread = new PacketParseThread();
+ mPktProcessThread.start();
+ for (Map.Entry entry : virtualDeviceManagers.entrySet()) {
+ entry.getValue().start();
+ }
+ mContext.unregisterReceiver(mPermissionResultReceiver);
+ }
+
+ public void stopProcess() {
+ mIsTaskRun = false;
+ try {
+ mPktProcessThread.join();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "stopProcess: failed to stop mPktProcessThread.", e);
+ }
+ for (Map.Entry entry : virtualDeviceManagers.entrySet()) {
+ entry.getValue().stop();
+ }
+ }
+
+ public void sendMsg(MsgHeader header, byte[] body, int deviceType) {
+ synchronized (mSendMsgLock) {
+ byte[] headerData = header.getData();
+ writeN(headerData, 0, headerData.length, deviceType);
+ if(body != null) {
+ writeN(body, 0, body.length, deviceType);
+ }
+ }
+ }
+
+ private int readN(byte[] data, int offset, int len) {
+ int readLen = 0;
+ while(len > 0) {
+ if(!mIsTaskRun) {
+ break;
+ }
+ int retLen = mVirtualDeviceIO.readN(data, offset, len);
+ if(retLen > 0) {
+ len -= retLen;
+ offset += retLen;
+ readLen += retLen;
+ continue;
+ }
+ try {
+ Thread.sleep(WAITING_INTERVAL);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "readN: sleep is interrupt", e);
+ }
+ }
+ return readLen;
+ }
+
+ private int writeN(byte[] data, int offset, int len, int deviceType) {
+ int writeLen = 0;
+ while(len > 0) {
+ if(!mIsTaskRun) {
+ break;
+ }
+ int retLen = mVirtualDeviceIO.writeN(data, offset, len, deviceType);
+ if(retLen > 0) {
+ len -= retLen;
+ offset += retLen;
+ writeLen += retLen;
+ continue;
+ }
+ try {
+ Thread.sleep(WAITING_INTERVAL);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "readN: sleep is interrupt", e);
+ }
+ }
+ return writeLen;
+ }
+
+ class PacketParseThread extends Thread {
+ byte[] header = new byte[MSG_HEADER_LEN];
+ @Override
+ public void run(){
+ while (mIsTaskRun) {
+ if (readN(header, 0, MSG_HEADER_LEN) != MSG_HEADER_LEN) {
+ Log.e(TAG, "Read msg header error");
+ continue;
+ }
+ MsgHeader msgHeader = new MsgHeader(header);
+ int bodyLen = msgHeader.mPayloadLength;
+ byte[] body = new byte[bodyLen];
+ if (readN(body, 0, bodyLen) != bodyLen) {
+ Log.e(TAG, "Read msg header error");
+ continue;
+ }
+ processMsg(msgHeader, body);
+ }
+ }
+ }
+
+ public static class MsgHeader {
+ public short mVersion;
+ public short mOptType;
+ public short mDeviceType;
+ public short mDeviceId;
+ public short mNextProtocol;
+ public short mHopLimit;
+ public int mPayloadLength;
+ private byte[] mData = null;
+
+ public MsgHeader(byte[] data) {
+ initParam(data);
+ mData = new byte[data.length];
+ System.arraycopy(data, 0, mData, 0, data.length);
+ }
+
+ public MsgHeader(short optType, short devType, short devId, int msgLen) {
+ mData = new byte[MSG_HEADER_LEN];
+ mData[0] = 0;
+ mData[1] = 0x01;
+
+ mData[2] = (byte) (optType >> 8);
+ mData[3] = (byte) (optType & 0x00FF);
+
+ mData[4] = (byte) (devType >> 8);
+ mData[5] = (byte) (devType & 0xFF);
+
+ mData[6] = (byte) (devId >> 8);
+ mData[7] = (byte) (devId & 0xFF);
+
+ mData[8] = (byte) ((msgLen & 0xFF000000) >> 24);
+ mData[9] = (byte) ((msgLen & 0x00FF0000) >> 16);
+ mData[10] = (byte) ((msgLen & 0x0000FF00) >> 8);
+ mData[11] = (byte) (msgLen & 0x000000FF);
+
+ mData[12] = 0x00;
+ mData[13] = 0x00;
+
+ mData[14] = 0x00;
+ mData[15] = 0x00;
+
+ initParam(mData);
+ }
+
+ private void initParam(byte[] data) {
+ mVersion = (short) ((data[0] << 8) | (data[1] & 0x0FF));
+ mOptType = (short) ((data[2] << 8) | (data[3] & 0x0FF));
+ mDeviceType = (short) ((data[4] << 8) | (data[5] & 0x0FF));
+ mDeviceId = (short) ((data[6] << 8) | (data[7] & 0x0FF));
+ mPayloadLength = ((data[8] << 24) | ((data[9] & 0x0FF) << 16)
+ | ((data[10] & 0x0FF) << 8) | (data[11] & 0x0FF)) - MSG_HEADER_LEN;
+ mNextProtocol = (short) ((data[12] << 8) | (data[13] & 0x0FF));
+ mHopLimit = (short) ((data[14] << 8) | (data[15] & 0x0FF));
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+
+ @Override
+ public String toString() {
+ return "MsgHeader{" +
+ "mVersion=" + mVersion +
+ ", mOptType=" + mOptType +
+ ", mDeviceType=" + mDeviceType +
+ ", mDeviceId=" + mDeviceId +
+ ", mPayloadLength=" + mPayloadLength +
+ ", mNextProtocol=" + mNextProtocol +
+ ", mHopLimit=" + mHopLimit +
+ "}";
+ }
+ }
+}
+
+
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java
new file mode 100644
index 0000000000000000000000000000000000000000..fca6edf5976b8a14a77370a1b66685dc70eac7f0
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophone.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.microphone;
+
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+import com.huawei.cloudphone.jniwrapper.OpusJNIWrapper;
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class VirtualMicrophone {
+ private static final String TAG = "VirtualMicrophone";
+ private static final int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
+ static final int DEFAULT_SAMPLE_RATE = 48000;
+ static final int DEFAULT_CHANNEL = 1;
+ static final int DEFAULT_BUFFER_SIZE = 320;
+ static final String SAMPLE_RATE = "sample_rate";
+ static final String MAX_FRAME_SIZE = "max_frame_size";
+ static final String CHANNEL = "channel";
+ static final String FORMAT = "format";
+
+ private AudioRecord mAudioRecord;
+ private int mRecordBuffSize = 320;
+ private int mMaxFrameSize;
+ private int mSampleRate;
+ private int mChannel;
+ private int mSampleFormat;
+ private boolean mIsReadTaskRunning;
+ private ReadThread mThread;
+ private IVirtualDeviceDataListener mListener;
+ private boolean mIsStart = false;
+
+ public VirtualMicrophone() {
+ mAudioRecord = null;
+ mRecordBuffSize = DEFAULT_BUFFER_SIZE;
+ mSampleRate = DEFAULT_SAMPLE_RATE;
+ mChannel = DEFAULT_CHANNEL;
+ mSampleFormat = AudioFormat.ENCODING_PCM_16BIT;
+ mIsReadTaskRunning = false;
+ mListener = null;
+ }
+
+ public int start() {
+ if (mIsStart) return 0;
+ if (OpusJNIWrapper.createOpusEncoder(mSampleRate, DEFAULT_CHANNEL) != 0) {
+ return -1;
+ }
+ mRecordBuffSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, AudioFormat.ENCODING_PCM_16BIT);
+ mAudioRecord = new AudioRecord(AUDIO_INPUT, mSampleRate, mChannel, mSampleFormat, mRecordBuffSize);
+ try {
+ mAudioRecord.startRecording();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "start failed, ", e);
+ return -1;
+ }
+ mIsReadTaskRunning = true;
+ mThread = new ReadThread();
+ mThread.start();
+ mIsStart = true;
+ return 0;
+ }
+
+ public int stop() {
+ if (!mIsStart) return 0;
+ mIsReadTaskRunning = false;
+ try {
+ if (mThread != null) {
+ mThread.join();
+ mThread = null;
+ }
+ mAudioRecord.stop();
+ } catch (IllegalStateException | InterruptedException e) {
+ Log.e(TAG, "stop failed, ", e);
+ return -1;
+ }
+
+ mAudioRecord.release();
+ mAudioRecord = null;
+ OpusJNIWrapper.destroyOpusEncoder();
+ mIsStart = false;
+ return 0;
+ }
+
+ public void setOnRecvDataListener(IVirtualDeviceDataListener listener) {
+ mListener = listener;
+ }
+
+ public int setParameters(Map params) {
+ try {
+ mSampleRate = Integer.parseInt(Objects.requireNonNull(params.get(SAMPLE_RATE)));
+ if (mSampleRate < 0 ) mSampleFormat = DEFAULT_SAMPLE_RATE;
+ mMaxFrameSize = Integer.parseInt(Objects.requireNonNull(params.get(MAX_FRAME_SIZE)));
+ mChannel = Integer.parseInt(Objects.requireNonNull(params.get(CHANNEL)));
+ if (Integer.parseInt(Objects.requireNonNull(params.get(FORMAT))) == 1)
+ mSampleFormat = AudioFormat.ENCODING_PCM_16BIT;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "setParameters: failed to set param", e);
+ return -1;
+ }
+ Log.i(TAG, "mSampleRate = " + mSampleRate + ", mMaxFrameSize = " + mMaxFrameSize +
+ ", mChannel = " + mChannel + ", mSampleFormat = " + mSampleFormat);
+ return 0;
+ }
+
+ class ReadThread extends Thread {
+ @Override
+ public void run() {
+ short[] bytes = new short[mRecordBuffSize];
+ byte[] outBuff = new byte[mRecordBuffSize];
+ while (mIsReadTaskRunning) {
+ mAudioRecord.read(bytes, 0, bytes.length);
+ if (mListener != null) {
+ int outBufLen = OpusJNIWrapper.opusEncode(bytes, bytes.length, outBuff);
+ if (outBufLen > 0) {
+ mListener.onRecvData(outBuff, 0, outBufLen);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..b80a81b620400dedab6b8817ff8c1d014cf2b452
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/microphone/VirtualMicrophoneManager.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.microphone;
+
+import static com.huawei.cloudphone.jniwrapper.JNIWrapper.MICROPHONE_DATA;
+import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN;
+import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.CHANNEL;
+import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.FORMAT;
+import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.MAX_FRAME_SIZE;
+import static com.huawei.cloudphone.virtualdevice.microphone.VirtualMicrophone.SAMPLE_RATE;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class VirtualMicrophoneManager extends VirtualDeviceManager {
+ private static final String TAG = "VirtualMICManager";
+ private static final short DEV_MIC_ID = 0;
+
+ private static final short OPT_MICROPHONE_START_RECORD_REQ = 0x1;
+ private static final short OPT_MICROPHONE_START_RECORD_RSP = 0x1001;
+ private static final short OPT_MICROPHONE_STOP_RECORD_REQ = 0x2;
+ private static final short OPT_MICROPHONE_STOP_RECORD_RSP = 0x1002;
+ private static final short OPT_MICROPHONE_SET_PARAM_REQ = 0x3;
+ private static final short OPT_MICROPHONE_SET_PARAM_RSP = 0x1003;
+ private static final short OPT_MICROPHONE_DATA = 0x10;
+
+ private static final int RSP_RESULT_LENGTH = 2;
+ private static final int RSP_AUDIO_TYPE_LENGTH = 2;
+ public static final String GRANT_MICROPHONE_PERMISSION_SUCCESS_ACTION = "android.intent.action.GRANT_MICROPHONE_PERMISSION_SUCCESS";
+
+ private VirtualMicrophone mVirtualMicrophone;
+ private VirtualDeviceProtocol mVirtualDeviceProtocol;
+ private Context mContext;
+
+ public VirtualMicrophoneManager(VirtualDeviceProtocol virtualDeviceProtocol, Context context) {
+ mVirtualDeviceProtocol = virtualDeviceProtocol;
+ mVirtualMicrophone = new VirtualMicrophone();
+ mContext = context;
+ }
+
+ public void processMsg(MsgHeader header, byte[] msgBody) {
+ switch (header.mOptType) {
+ case OPT_MICROPHONE_START_RECORD_REQ:
+ Log.i(TAG, "processMsg: start record");
+ handleStartRecordReq(msgBody);
+ break;
+ case OPT_MICROPHONE_STOP_RECORD_REQ:
+ Log.i(TAG, "processMsg: stop record");
+ handleStopRecordReq(msgBody);
+ break;
+ case OPT_MICROPHONE_SET_PARAM_REQ:
+ Log.i(TAG, "processMsg: set param");
+ handleSetParamReq(msgBody);
+ break;
+ }
+ }
+
+ public void initMicrophone() {
+ mVirtualMicrophone.setOnRecvDataListener(new MicrophoneDataListener());
+ if (mVirtualMicrophone.start() != 0) {
+ Log.e(TAG, "initMicrophone: failed to start microphone");
+ }
+ }
+
+ private void handleStartRecordReq(byte[] msgBody) {
+ Context context = mContext.getApplicationContext();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
+ if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions((Activity) mContext,
+ new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ DEV_TYPE_MICROPHONE);
+ } else {
+ initMicrophone();
+ }
+ }
+
+ private void handleStopRecordReq(byte[] msgBody) {
+ int result = mVirtualMicrophone.stop();
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH;
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH];
+ rspBody[0] = 0x0;
+ rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1);
+ MsgHeader rspHeader = new MsgHeader(OPT_MICROPHONE_STOP_RECORD_RSP, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(rspHeader, rspBody, MICROPHONE_DATA);
+ }
+
+ private void handleSetParamReq(byte[] msgBody) {
+ int maxFrameSize = (msgBody[0] << 8) | (msgBody[1] & 0x0FF);
+ int format = (msgBody[2] << 8) | (msgBody[3] & 0x0FF);
+ int sampleRate = (msgBody[4] << 8) | (msgBody[5] & 0x0FF);
+ int channel = (msgBody[6] << 8) | (msgBody[7] & 0x0FF);
+ Log.i(TAG, "mSampleRate = " + sampleRate + ", mMaxFrameSize = " + maxFrameSize +
+ ", mChannel = " + channel + ", mSampleFormat = " + format);
+
+ Map paramMap = new HashMap<>();
+ paramMap.put(FORMAT, Integer.toString(format));
+ paramMap.put(MAX_FRAME_SIZE, Integer.toString(maxFrameSize));
+ paramMap.put(SAMPLE_RATE, Integer.toString(sampleRate));
+ paramMap.put(CHANNEL, Integer.toString(channel));
+
+ int result = mVirtualMicrophone.setParameters(paramMap);
+ byte[] rspBody = new byte[RSP_RESULT_LENGTH];
+ rspBody[0] = 0x0;
+ rspBody[1] = (byte) (result == 0 ? 0x0 : 0x1);
+
+ int rspMsgLen = MSG_HEADER_LEN + RSP_RESULT_LENGTH;
+ MsgHeader header = new MsgHeader(OPT_MICROPHONE_SET_PARAM_RSP, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(header, rspBody, MICROPHONE_DATA);
+ }
+
+ public void stop() {
+ mVirtualMicrophone.stop();
+ }
+
+ public class MicrophoneDataListener implements IVirtualDeviceDataListener {
+
+ @Override
+ public void onRecvData(Object... args) {
+ byte[] data = (byte[]) args[0];
+ int offset = (int) args[1];
+ int length = (int) args[2];
+ int rspMsgLen = length + MSG_HEADER_LEN;
+ MsgHeader header = new MsgHeader(OPT_MICROPHONE_DATA, DEV_TYPE_MICROPHONE, DEV_MIC_ID, rspMsgLen);
+ byte[] rspBody = new byte[length];
+ System.arraycopy(data, offset, rspBody, 0, length);
+ mVirtualDeviceProtocol.sendMsg(header, rspBody, MICROPHONE_DATA);
+ }
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java
new file mode 100644
index 0000000000000000000000000000000000000000..9850e196da9c1ba21a281e5677e73484c8d9a76a
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensor.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.sensor;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+
+public class VirtualSensor implements SensorEventListener {
+ private static final String TAG = "VirtualSensor";
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+ private Sensor mAccelerationSensor;
+ private IVirtualDeviceDataListener mListener = null;
+ private HandlerThread mHandlerThread;
+ private boolean mIsStart = false;
+
+ public VirtualSensor(SensorManager sensorManager) {
+ mSensorManager = sensorManager;
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ mAccelerationSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ void registerSensorDataListener(IVirtualDeviceDataListener listener) {
+ mListener = listener;
+ }
+
+ public void startProcess() {
+ Log.i(TAG, "startProcess");
+ if (mIsStart) {
+ return;
+ }
+ mHandlerThread = new HandlerThread("sensorThread");
+ mHandlerThread.start();
+ Handler handler = new Handler(mHandlerThread.getLooper());
+ mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_GAME, handler);
+ mSensorManager.registerListener(this, mAccelerationSensor, SensorManager.SENSOR_DELAY_GAME, handler);
+ mIsStart = true;
+ }
+
+ public void stopProcess() {
+ Log.i(TAG, "stopProcess");
+ if (!mIsStart) {
+ return;
+ }
+ mSensorManager.unregisterListener(this, mSensor);
+ mSensorManager.unregisterListener(this, mAccelerationSensor);
+ try {
+ mHandlerThread.join();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "stopProcess: failed to stop mHandlerThread", e);
+ }
+ mIsStart = false;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent sensorEvent) {
+ if (mListener == null) {
+ return;
+ }
+ float x = sensorEvent.values[0];
+ float y = sensorEvent.values[1];
+ float z = sensorEvent.values[2];
+ int type = sensorEvent.sensor.getType();
+ if (type == Sensor.TYPE_ACCELEROMETER) {
+ mListener.onRecvData(x, y, z, sensorEvent.accuracy, 0);
+ } else if (type == Sensor.TYPE_GYROSCOPE) {
+ mListener.onRecvData(x, y, z, sensorEvent.accuracy, 1);
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int i) {
+ }
+}
diff --git a/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..c363a67e4aee6741349a214f5ffde461f6fe3345
--- /dev/null
+++ b/cloudphone/src/main/java/com/huawei/cloudphone/virtualdevice/sensor/VirtualSensorManager.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
+ *
+ * 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
+ *
+ * http://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.
+ */
+package com.huawei.cloudphone.virtualdevice.sensor;
+
+import static com.huawei.cloudphone.jniwrapper.JNIWrapper.SENSOR_DATA;
+import static com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MSG_HEADER_LEN;
+
+import android.hardware.SensorManager;
+import android.util.Log;
+
+import com.huawei.cloudphone.virtualdevice.common.IVirtualDeviceDataListener;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceManager;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol;
+import com.huawei.cloudphone.virtualdevice.common.VirtualDeviceProtocol.MsgHeader;
+
+public class VirtualSensorManager extends VirtualDeviceManager {
+ private static final String TAG = "VirtualSensorManager";
+
+ public static final short DEV_GYROSCOPE = 0x0;
+ public static final short DEV_ACCELERATION = 0x1;
+ public static final short OPT_SENSOR_ENABLE_REQ = 0x1;
+ public static final short OPT_SENSOR_ENABLE_RSP = 0x1001;
+ public static final short OPT_SENSOR_DISABLE_REQ = 0x2;
+ public static final short OPT_SENSOR_DISABLE_RSP = 0x1002;
+ public static final short OPT_SENSOR_DATA = 0x10;
+
+ private VirtualSensor mVirtualSensor;
+ private VirtualDeviceProtocol mVirtualDeviceProtocol;
+
+ public VirtualSensorManager(VirtualDeviceProtocol virtualDeviceProtocol, SensorManager sensorManager) {
+ mVirtualDeviceProtocol = virtualDeviceProtocol;
+ mVirtualSensor = new VirtualSensor(sensorManager);
+ }
+
+ public void processMsg(MsgHeader header, byte[] body) {
+ switch (header.mOptType) {
+ case OPT_SENSOR_ENABLE_RSP:
+ Log.i(TAG, "processMsg: enable sensor");
+ mVirtualSensor.registerSensorDataListener(new SensorDataListener());
+ mVirtualSensor.startProcess();
+ break;
+ case OPT_SENSOR_DISABLE_RSP:
+ Log.i(TAG, "processMsg: disable sensor");
+ mVirtualSensor.stopProcess();
+ break;
+ default:
+ Log.e(TAG, "processMsg: error opt type");
+ }
+ }
+
+ public void start() {
+ int rspMsgLen = MSG_HEADER_LEN;
+ MsgHeader header = new MsgHeader(OPT_SENSOR_ENABLE_REQ, DEV_TYPE_SENSOR, DEV_GYROSCOPE, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA);
+ header = new MsgHeader(OPT_SENSOR_ENABLE_REQ, DEV_TYPE_SENSOR, DEV_ACCELERATION, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA);
+ }
+
+ public void stop() {
+ int rspMsgLen = MSG_HEADER_LEN;
+ MsgHeader header = new MsgHeader(OPT_SENSOR_DISABLE_REQ, DEV_TYPE_SENSOR, DEV_GYROSCOPE, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA);
+ header = new MsgHeader(OPT_SENSOR_DISABLE_REQ, DEV_TYPE_SENSOR, DEV_ACCELERATION, rspMsgLen);
+ mVirtualDeviceProtocol.sendMsg(header, null, SENSOR_DATA);
+ }
+
+ class SensorDataListener implements IVirtualDeviceDataListener {
+ @Override
+ public void onRecvData(Object... args) {
+ String xStr = Float.valueOf((float) args[0]).toString();
+ String yStr = Float.valueOf((float) args[1]).toString();
+ String zStr = Float.valueOf((float) args[2]).toString();
+ String accurateStr = Integer.valueOf((int) args[3]).toString();
+ String body = xStr + ":" + yStr + ":" + zStr + ":" + accurateStr + ":";
+ int type = (int) args[4];
+ int bodyLen = body.getBytes().length;
+ int rspMsgLen = bodyLen + MSG_HEADER_LEN;
+ MsgHeader header = new MsgHeader(OPT_SENSOR_DATA, DEV_TYPE_SENSOR, (short)type, rspMsgLen);
+ byte[] rspBody = new byte[bodyLen];
+ System.arraycopy(body.getBytes(), 0, rspBody, 0, bodyLen);
+ mVirtualDeviceProtocol.sendMsg(header, rspBody, SENSOR_DATA);
+
+ }
+ }
+}