I am trying to use this in react native app. I have added mobility and commonly as a module dependency. I have updated by settings.gradle as well.
build.gradle (app)
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
import com.android.build.OutputFile
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
enableHermes: false, // clean and rebuild if changing
]
apply from: "../../node_modules/react-native/react.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and that value will be read here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for in debug.
*/
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.smartdoctor.zoom"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
multiDexEnabled true
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
signingConfigs {
common {
storeFile file('../keystore/smart_doctor.jks')
storePassword 'smartdoctor.2022'
keyAlias 'smartdoctor'
keyPassword 'smartdoctor.2022'
}
}
buildTypes {
debug {
signingConfig signingConfigs.common
if (nativeArchitectures) {
ndk {
abiFilters nativeArchitectures.split(',')
}
}
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.common
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
}
}
}
// Zoom Video Sdk
packagingOptions {
pickFirst '**/*.so'
pickFirst '**/*.aidl'
pickFirst 'META-INF/rich-sdk_release.kotlin_module'
// pickFirst 'lib/armeabi-v7a/libc++_shared.so'
// pickFirst 'lib/arm64-v8a/libc++_shared.so'
// pickFirst 'lib/x86/libc++_shared.so'
// pickFirst 'lib/x86_64/libc++_shared.so'
}
}
dependencies {
//implementation fileTree(dir: "libs", include: ["*.jar"])
implementation fileTree(dir: "libs", include: ["*.aar"])
//noinspection GradleDynamicVersion
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
// //Zoom
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha02'
implementation 'com.google.crypto.tink:tink-android:1.5.0'
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'androidx.multidex:multidex:2.0.0'
implementation project(':mobilertc')
implementation project(':commonlib')
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.implementation
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
This is my project structure
This is my native module i.e Zoom Module
package com.smartdoctor;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;
import us.zoom.sdk.JoinMeetingOptions;
import us.zoom.sdk.JoinMeetingParams;
import us.zoom.sdk.MeetingError;
import us.zoom.sdk.MeetingParameter;
import us.zoom.sdk.MeetingService;
import us.zoom.sdk.MeetingServiceListener;
import us.zoom.sdk.MeetingStatus;
import us.zoom.sdk.StartMeetingOptions;
import us.zoom.sdk.StartMeetingParamsWithoutLogin;
import us.zoom.sdk.ZoomError;
import us.zoom.sdk.ZoomSDK;
import us.zoom.sdk.ZoomSDKInitParams;
import us.zoom.sdk.ZoomSDKInitializeListener;
public class ZoomModule extends ReactContextBaseJavaModule implements ZoomSDKInitializeListener, MeetingServiceListener {
private final ReactApplicationContext reactContext;
private Promise initPromise;
private Promise meetingPromise;
ZoomModule(ReactApplicationContext context) {
super(context);
this.reactContext = context;
}
@NonNull
@Override
public String getName() {
// this defines name of how we will import this module in JS
return "ZoomModule";
}
@ReactMethod
public void initZoom(String publicKey, String privateKey, String domain, Promise promise) {
Log.d(this.getName(), "Init zoom: " + publicKey
+ " and privateKey: " + privateKey + " domain: " + domain);
try {
initPromise = promise;
this.getReactApplicationContext().getCurrentActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
ZoomSDK zoomSDK = ZoomSDK.getInstance();
ZoomSDKInitParams initParams = new ZoomSDKInitParams();
initParams.appKey = publicKey;
initParams.appSecret = privateKey;
initParams.enableGenerateDump = true;
initParams.enableLog = true;
initParams.domain = domain;
zoomSDK.initialize(reactContext.getCurrentActivity(),ZoomModule.this, initParams);
}
});
} catch (Exception e) {
Log.e("ERR_UNEXPECTED_EXCEPTIO", e.getMessage());
promise.reject("ERR_UNEXPECTED_EXCEPTIO", e);
}
}
@Override
public void onZoomSDKInitializeResult(int errorCode, int internalErrorCode) {
Log.d(this.getName(), "Init Zoom Result with : errorCode " + errorCode
+ " and internalErrorCode: " + internalErrorCode);
if(errorCode == ZoomError.ZOOM_ERROR_SUCCESS) {
Log.d(this.getName(), "Initializing meeting service SUCCESSFUL");
ZoomSDK zoomSDK = ZoomSDK.getInstance();
MeetingService meetingService = zoomSDK.getMeetingService();
if(meetingService != null) {
Log.d(this.getName(), "Adding listener for meeting service ");
meetingService.addListener(this);
}
//here we should notify JS
initPromise.resolve("Zoom initialized");
}
}
@Override
public void onZoomAuthIdentityExpired() {
}
@Override
public void onMeetingStatusChanged(MeetingStatus meetingStatus, int errorCode, int internalErrorCode) {
Log.d(this.getName(), "Meeting Status Changed meetingStatus : " + meetingStatus
+ "errorCode: " + errorCode + " and internalErrorCode: " + internalErrorCode);
switch(meetingStatus) {
case MEETING_STATUS_FAILED: {
break;
}
case MEETING_STATUS_DISCONNECTING: {
break;
}
case MEETING_STATUS_INMEETING: {
break;
}
case MEETING_STATUS_IN_WAITING_ROOM: {
break;
}
}
}
@Override
public void onMeetingParameterNotification(MeetingParameter meetingParameter) {
}
@ReactMethod
public void joinMeeting(String displayName, String meetingNumber, String password, Promise promise) {
Log.d("ou shit", "Join meeting called : displayName " + displayName
+ " and meetingNumber: " + meetingNumber + " password" + password);
ZoomSDK zoomSDK = ZoomSDK.getInstance();
if(!zoomSDK.isInitialized()) {
promise.reject("ERR_ZOOM_START", "ZoomSDK has not been initialized successfully");
return;
}
MeetingService meetingService = zoomSDK.getMeetingService();
if(meetingService == null) {
promise.reject("ERR_ZOOM_START", "Zoom MeetingService has not been initialized successfully");
return;
}
JoinMeetingOptions opts = new JoinMeetingOptions();
JoinMeetingParams params = new JoinMeetingParams();
params.displayName = displayName;
params.meetingNo = meetingNumber;
params.password = password;
try {
int joinMeetingResult = meetingService.joinMeetingWithParams(this.reactContext, params,opts);
Log.i(this.getName(), "joinMeeting, joinMeetingResult=" + joinMeetingResult);
if (joinMeetingResult != MeetingError.MEETING_ERROR_SUCCESS) {
promise.reject("ERR_ZOOM_JOIN", "joinMeeting, errorCode=" + joinMeetingResult);
}
meetingPromise = promise;
} catch (Exception e) {
promise.reject("JoinMeetingException", e);
}
}
@ReactMethod
public void startMeeting(String meetingNumber, String username, String userId, String jwtAccessToken, String jwtApiSecretKey, Promise promise) {
Log.d(this.getName(), "Start meeting called with meetingNumber: " + meetingNumber
+ " and username: " + username + " userId " + userId + "jwtAccessToken" + jwtAccessToken + "jwtApiKey" + jwtApiSecretKey);
try {
meetingPromise = promise;
ZoomSDK zoomSDK = ZoomSDK.getInstance();
if(!zoomSDK.isInitialized()) {
promise.reject("ERR_ZOOM_START", "ZoomSDK has not been initialized successfully");
return;
}
final MeetingService meetingService = zoomSDK.getMeetingService();
if(meetingService.getMeetingStatus() != MeetingStatus.MEETING_STATUS_IDLE) {
long lMeetingNo = 0;
try {
lMeetingNo = Long.parseLong(meetingNumber);
} catch (NumberFormatException e) {
promise.reject("ERR_ZOOM_START", "Invalid meeting number: " + meetingNumber);
return;
}
if(meetingService.getCurrentRtcMeetingNumber() == lMeetingNo) {
meetingService.returnToMeeting(reactContext.getCurrentActivity());
promise.resolve("Already joined zoom meeting");
return;
}
}
StartMeetingOptions opts = new StartMeetingOptions();
StartMeetingParamsWithoutLogin params = new StartMeetingParamsWithoutLogin();
params.displayName = username;
params.meetingNo = meetingNumber;
params.userId = userId;
params.userType = MeetingService.USER_TYPE_API_USER;
// ZAK - Zoom Access Token
String zak = this.getZak(userId, jwtAccessToken, jwtApiSecretKey);
if(zak == null){
promise.reject("StartMeetingFail", "ZAK is null or was not retrieved");
return;
}
params.zoomAccessToken = zak;
int startMeetingResult = meetingService.startMeetingWithParams(reactContext.getCurrentActivity(), params, opts);
Log.i(this.getName(), "startMeeting, startMeetingResult=" + startMeetingResult);
if (startMeetingResult != MeetingError.MEETING_ERROR_SUCCESS) {
promise.reject("ERR_ZOOM_START", "startMeeting, errorCode=" + startMeetingResult);
}
promise.resolve("Meeting Started");
} catch (Exception ex) {
promise.reject("ERR_UNEXPECTED_EXCEPTION", ex);
}
}
// put in into something like ZakUtils class to make it clean ??
private String getZak(String userId, String jwtApiKey, String jwtApiSecret){
String jwtAccessToken = this.createJWTAccessToken(jwtApiKey, jwtApiSecret);
String zak = this.getZoomAccessToken(userId, jwtAccessToken);
return zak;
}
public String getZoomAccessToken(String userId, String jwtAccessToken) {
// String jwtAccessToken = createJWTAccessToken();
if(jwtAccessToken == null || jwtAccessToken.isEmpty())
return null;
// Create connection
try {
URL zoomTokenEndpoint = new URL("https://api.zoom.us/v2/users/" + userId + "/token?type=zak&access_token=" + jwtAccessToken);
HttpsURLConnection connection = (HttpsURLConnection) zoomTokenEndpoint.openConnection();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream responseBody = connection.getInputStream();
InputStreamReader responseBodyReader = new InputStreamReader(responseBody, "UTF-8");
BufferedReader streamReader = new BufferedReader(responseBodyReader);
StringBuilder responseStrBuilder = new StringBuilder();
//get JSON String
String inputStr;
while ((inputStr = streamReader.readLine()) != null)
responseStrBuilder.append(inputStr);
connection.disconnect();
JSONObject jsonObject = new JSONObject(responseStrBuilder.toString());
return jsonObject.getString("token");
} else {
Log.d(this.getName(), "error in connection");
return null;
}
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e){
e.printStackTrace();
}
return null;
}
public String createJWTAccessToken(final String API_KEY, final String API_SECRET) {
long EXPIRED_TIME= 3600 * 2;
long time=System.currentTimeMillis()/1000 + EXPIRED_TIME;
String header = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}";
String payload = "{\"iss\": \"" + API_KEY + "\"" + ", \"exp\": " + String.valueOf(time) + "}";
try {
String headerBase64Str = Base64.encodeToString(header.getBytes("utf-8"), Base64.NO_WRAP| Base64.NO_PADDING | Base64.URL_SAFE);
String payloadBase64Str = Base64.encodeToString(payload.getBytes("utf-8"), Base64.NO_WRAP| Base64.NO_PADDING | Base64.URL_SAFE);
final Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(API_SECRET.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);
byte[] digest = mac.doFinal((headerBase64Str + "." + payloadBase64Str).getBytes());
return headerBase64Str + "." + payloadBase64Str + "." + Base64.encodeToString(digest, Base64.NO_WRAP| Base64.NO_PADDING | Base64.URL_SAFE);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
}