Android版本
📌
Android版本1.53.311
一. 准备阶段
应用需要用到AndroidX支持库,如果在使用Android Support库的请迁移,如果已经使用AndroidX请忽略。 迁移可参考下面的说明,或查阅 AndroidX迁移指南: |
1) 切换到单独的分支,迁移完成前最好不进行功能开发,然后将代码备份一份,防止迁移失败
2) 检查项目中每个module中support库的依赖,将版本升级到28.0.0,需要将compileSdkVersion升级到28,保证项目正常能编译通过
3) 在Android Studio中Refactor->Migrate to AndroidX完成迁移
4) 编译项目,如果有编译错误,解决即可(如第三方依赖库版本不支持androidx需要升级到新版本,例如ButterKnife)
二. 配置伯索的maven仓库
在项目的build.gradle或settings.gradle文件中添加伯索的maven仓库。
repositories {
...
maven { url 'https://nexus.plaso.cn/repository/maven-public/'}
}
三. app->build.gradle配置
在项目的app->build.gradle 中:①配置minSdkVersion需要大于等于24,targetSdkVersion 大于等于33 ②添加SDK相关的依赖。
android {
……
defaultConfig {
……
minSdkVersion 24 // 配置 >= 24
targetSdkVersion 33 // 配置 >= 33
……
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
...
implementation( 'cn.plaso:styleupime:$version')
}
四. 实现IResourceProvider
IResourceProvider是定制化资料中心接口, APP提供该接口的实现,并传入SDK。IResourceProvider实现后我们需要创建一个StyleUpime对象,创建StyleUpime对象需要用到IResourceProvider,IResourceProvider不能为null,后续启动实时课堂需要使用StyleUpime对象。
override fun onCreate() {
super.onCreate()
...
resProvider = object : IResourceProvider {
override fun supportResourceCenter(): Boolean {
//是否支持定制化资料中心,返回true代表支持,false代表不支持
return false
}
override fun showResourceCenter(p0: Boolean) {
// SDK用户忽略 p0参数;
//显示 APP 定制化资料中心,在此处实现跳转到定制化资料中心页面
}
override fun dismissResourceCenter() {
//关闭 APP 定制化资料中心
}
override fun getExtFileName(extFileId: Any?, resouceCallback:IResourceCallback?) {
//APP 根据传入的文件标识内容返回其可以访问的完整 URL
}
override fun signQuery(queryMap: MutableMap<String, Any>?, cb: SignCallback?): Boolean {
//请求SDK Client进行参数签名,窗口模式下,插入文件,需要从服务器获取签名
if (queryMap != null) {
queryMap["appId"] = Config.appId //Config.appId填写从伯索获取的appId,这个参数是必需的
queryMap["validTime"] = 120
queryMap["validBegin"] = System.currentTimeMillis() / 1000
//根据queryMap,appKey生成一个query
val signedQuery = SignHelper.sign(queryMap, Config.appKey) //Config.appKey填写从伯索获取的appKey
cb?.onSignCompleted(signedQuery)
} else {
cb?.onSignCompleted(null)
}
return true
}
override fun getWebviewUrl(type: Int, callback: IUrlCallback?) {
//请求SDK Client是否有教材包的链接需要在课堂内打开
callback?.onGetUrl("https://www.plaso.cn")
}
}
//创建一个StyleUpime对象
upime = StyleUpime.create(this, "demo", resProvider)
}
在signQuery方法中我们需要生成一个query,生成方法参考如下形式:
object SignHelper {
/**
* Sign the [params] with specific [signKey]
*/
fun sign(params: MutableMap<String, Any>, signKey: String): String {
val keys = params.keys.sorted()//将Map中的key按顺序排列
val sortedParams = StringBuilder()
for ((index, key) in keys.withIndex()) {
if (index != 0) {
sortedParams.append("&")
}
sortedParams.append("$key=${params[key]}")
}
val signature = encrypt(sortedParams.(), signKey)
params["signature"] = signature;
return buildQuery(params);
}
private fun buildQuery(params: MutableMap<String, Any>): String {
val result = StringBuilder()
for ((index, key) in params.keys.withIndex()) {
if (index != 0) {
result.append("&")
}
result.append("$key=${URLEncoder.encode(params[key].toString(), "UTF-8")}")
}
return result.()
}
private fun encrypt(encryptText: String, signKey: String): String {
val algorithm = "HmacSHA1"
val charset = Charset.forName("UTF-8")
val data = signKey.toByteArray(charset)
val secretKey = SecretKeySpec(data, algorithm)
val mac = Mac.getInstance(algorithm)
mac.init(secretKey)
val rst = mac.doFinal(encryptText.toByteArray(charset))
return BigInteger(1, rst).(16).toUpperCase()
}
五. 启动实时课堂
实时课堂支持配置日志和水印,在启动实时课堂之前我们可以根据自己的需要进行设置,如不需要可以忽略
private fun setUpimeParameter(){
var parameter : UpimeParameter = UpimeParameter()
parameter.waterMark = "waterMark"
parameter.waterMarkSize = size
parameter.logDir = logPath
parameter.logLevel = UpimeParameter.DEBUG
upime.setUpimeParameter(parameter) //upime是step.4创建的StyleUpime对象
}
UpimeParameter参数说明:
参数名 | 参数类型 | 是否必须 | 默认值 | 参数说明 |
---|---|---|---|---|
appid | String | 否 | - | 应用唯一标识 |
waterMark | String | 否 | - | 水印 |
logDir | String | 否 | - | 日志路径 |
logLevel | int | 否 | -1 | 日志级别 DEBUG:0 INFO:1 WARN:2 ERROR:3 |
启动实时课堂时也需要生成一个query,query中包含创建实时课堂所需要的一些参数,query里面如果包含中文字符串,中文字符串必须进行utf8编码,可以参考demo SignHelper里面buildQuery函数的编码方式。 query创建方法参考以下形式:
private fun getQuery(): String? {
...
val params = mutableMapOf<String, Any>().also {
it["appId"] = Config.appId //Config.appId填写从伯索获取的appId
it["appType"] = "liveclassSDK"
it["beginTime"] = System.currentTimeMillis() / 1000
it["endTime"] = endTime
it["mediaType"] = meetingType
it["meetingId"] = meetingId
it["meetingType"] = if (useMeetingMode) "meeting" else "public"
it["loginName"] = userName
it["userName"] = userName
it["userType"] = userType
it["validTime"] = 1080000
it["onlineMode"] = Integer.parseInt(onlineMode)
it["d_sharpness"] = sharpness
it["d_dimension"] = resolution
if (videoStream != -1) {
it["videoStream"] = videoStream
}
it["vendorType"] = rtcType
it["enableNewClassExam"] = enableNewClassExam
}
//SignHelper.sign方法参考step.4,Config.appKey填写从伯索获取的appKey
return SignHelper.sign(params, Config.appKey)
}
query参数说明:
参数名 | 参数类型 | 是否必须 | 默认值 | 参数说明 |
---|---|---|---|---|
appId | String | 是 | - | 应用唯一标识 |
appType | String | 否 | - | 应用类型 |
beginTime | Long | 是 | - | 课堂开始时间 |
endTime | Int | 否 | - | 课堂结束时间(s) |
mediaType | String | 是 | - | 上课类型视频课堂传:“video” ;音频传:“audio” |
meetingId | String | 是 | - | 会议号,可随意定义 |
meetingType | String | 是 | - | 是不是会议模式 ;非会议模式传:"public" 会议模式传:"meeting" |
loginName | String | 是 | - | 唯一标识该用户的id,不能为空,相同的loginName登录,后面一个会使前面一个登出; |
userName | String | 否 | - | 用户名 |
userType | String | 是 | - | 用户类型 老师传:"speaker" 学生传:"listener" 游客传:"visitor" |
validTime | Int | 是 | - | 有效时间 |
onlineMode | Int | 是 | - | 上台人数 |
d_sharpness | String | 是 | - | 清晰度 VD_360P传:"10" VD_720P传:"20" VD_1080P传:"30" |
d_dimension | String | 是 | - | 分辨率 ;参考格式:"1280x720" |
enableNewClassExam | int | 否 | 0 | 老版随堂测:0;新版随堂测选择题:1;新版随堂测填空题:2;新版随堂测选择+填空:3 |
d_enableObjectEraser | int | 否 | 0 | 点擦为0, 对象擦:1(手写),3(手写+文本框),5(手写+图形),7(手写+文本框+图形) |
query生成完成后需要在启动实时课堂时传给SDK,需要构建一个ClassConfig对象,ClassConfig对象同样需要进行一些参数配置。ClassConfig对象创建完毕后只需要通过StyleUpime对象调用launchLiveClass方法即可启动实时课堂。
private fun launchLiveClass() {
...
val query = getQuery()
if (query != null) {
val config = ClassConfig().also {
it.classURL = query
it.host = Config.server //"https://dev.plaso.cn"
it.openFileMode = UpimeConfig.OPEN_FILE_MODE_WINDOW
it.toolboxItems = UpimeConfig.ToolBoxItem.ALL.value
it.supportBlueToothConnect = enableBlueTooth
it.enableInteractPpt = enablePptInteract
it.teachToolTypes = UpimeConfig.TeacherToolType.PureUpimeTeachToolTypeAll.value
it.supportUndo = undoSupport
it.useNewSmallBoard = useNewSmallBoard
it.supportSelect = supportSelect;
it.endRemindTime = remindTime.text.().toInt()
var limitnum = 0
if (!TextUtils.isEmpty(redPacketLimit.text?.())) {
limitnum = redPacketLimit.text.().toInt()
}
if (limitnum > 0) {
it.redPacketLimit = limitnum;
}
if (useMeetingMode && TextUtils.isDigitsOnly(etPermission.text.())) {
it.defaultPermission = etPermission.text.().toInt()
}
if (isPhoneTeachingMethod) {
it.mobileTeaching = true;
}
it.residentCamera = isResidentCamera;
it.auxiliaryCamera = isAuxiliaryCamera;
it.supportHighlighter = supportHighlighter;
it.forbiddenScreenShot = forbiddenScreenShot;
}
//upime是step.4创建的StyleUpime对象
upime.launchLiveClass(config, object : ILiveClassListener {
override fun onLiveClassReady(upimeBoard: UpimeBoard?) {
// 获得UpimeBoard对象,可用于资料中心的资料插入等操作
}
override fun onExited(exitCode: Int, mid: String) {
//退出实时课堂后回调
}
override fun onSkinChanged(skinId: Int) {
//实时课堂更换背景回调
}
})
}
}
Classconfig参数说明:
参数名 | 参数类型 | 是否必须 | 默认值 | 参数说明 |
---|---|---|---|---|
classURL | string | 是 | - | 实时课堂配置参数query ;如果query里面有中文字符串,此中文字符串需要编码后放到query里传入。 |
host | string | 否 | - | 服务地址 |
userName | string | 否 | - | 当前用户名称 |
classMember | ArrayList | 否 | - | 课堂成员列表 |
allowLocalPPT | boolean | 否 | true | 是否允许在实时课堂中插入本地PPT |
enableSendMessage | boolean | 否 | true | 消息界面是否显示消息输入框 |
hideOtherClient | boolean | 否 | false | 是否显示学生不在班级的所有学生和助教 |
endRemindTime | int | 否 | 0 | 实时课堂添加双减的提示时间,到达这个时间之后,会提示用户 (:秒) |
redPacketLimit | int | 否 | 0 | 红包雨个数限制 |
defaultPermission | int | 否 | 0 | 会议模式默认权限 |
supportSelect | boolean | 否 | true | 是否支持工具栏选择箭头 |
openFileMode | int | 否 | null | 文件打开模式 |
toolboxItems | int | 否 | - | 百宝箱工具条目配置(工具请参考UpimeConfig.ToolBoxItem) |
enableInteractPpt | boolean | 否 | false | 是否开启PPT交互 |
teachToolTypes | int | 否 | 0 | 教具类型支持的类型(类型请参考UpimeConfig.TeacherToolType |
supportUndo | boolean | 否 | false | 是否支持撤销操作 |
useNewSmallBoard | boolean | 否 | false | 是否使用新版小黑板 |
supportHighlighter | Boolean | 否 | false | 是否使用荧光笔 |
forbiddenScreenShot | Boolean | 否 | false | 是否防止课堂截屏 |
enableVote | Boolean | 否 | false | 是否支持投票工具 |
enableSaveBoard | Boolean | 否 | false | 是否支持保存板书功能,如果支持需要对接实现onUpimeBoardSaved这个回调,保存数据到自己三方服务器 |
六. 参考代码Demo
请点击下载
使用步骤
- 解压缩
- 修改build.gradle文件,升级版本到1.53.311
dependencies {
……
implementation 'cn.plaso:styleupime:1.53.311
'
}
- 修改文件: app/src/main/java/cn/plaso/liveclasssdkdemo/Config.kt, 填入伯索分配的appid和key后即可体验。
var appId: String = "<your-appId>"
/**
* appKey
*/
const val appKey:String = "<your-app-key>"
七.资料中心接入事项
一:
1.资料中心选择好的文件传参时,服务器传过来的文件路径不用带签名计算,签名需要的时候 ,实时签名。
2.insertObject里面UpimeObject.info根据type传string路径或者list;
例如:TYPE_IMAGE则直接把path传进去;
如果服务器传过来的地址是没有做计算签名的,那么这个时候需要客户端把把签名加上。
例如:TYPE_PPT,根据type传list, 服务器传过来的没有签名,则在getExtFileName回调里面把签名加上。
例如:TYPE_VIDEO,服务器传过来的没有签名,则在getExtFileName里面把签名加上。
二:
如果type传list,会回调到这个函数getExtFileName里,需要在getExtFileName里面做签名;
如果传string ,则不会回调到getExtFileName,签名根据 "步骤一" 操作计算。
八. 对接注意事项
在DemoApp.kt文件中传入appId 和appKey 值,详情参考下文例子;签名请通过在服务器中完成,Demo 中只是样例。
override fun signQuery(queryMap: MutableMap<String, Any>?, cb: SignCallback?): Boolean {
if (queryMap != null) {
queryMap["appId"] = xxx //自己机构的appId
queryMap["validTime"] = 120
queryMap["validBegin"] = System.currentTimeMillis() / 1000
// TODO get it from server
// this is only for test
val signedQuery = SignHelper.sign(queryMap, xxx) //自己机构的appKey
cb?.onSignCompleted(signedQuery)
} else {
cb?.onSignCompleted(null)
}
return true
}
(2). AndroidManifest.xml添加下面代码,不然课堂里面截图至相册功能可能会失败
<meta-data
android:name="ScopedStorage"
android:value="true" />
(3). targetSdkVersion 设置成 33;
