抖音直播Android SDK接入说明
写在前面
目前抖音直播SDK,仅对有一定流量基础的应用开放,若您的应用已经有一定的日活用户或属于某个行业内的垂类应用,
请您前往抖音开放平台工单系统联系客服,获取相关信息,若您符合接入条件,客服会联系您并提供直播接入相关参数。
接入流程

一、接入SDK
1.1 特别注意
- SDK包您将通过技术支持获得,暂不提供公开的SDK
- 直播要求最低andorid版本5.0 (minsdkversion 21),请注意检查是否符合要求
1.2 引入maven仓库
allprojects {
repositories {
maven {
url "https://artifact.bytedance.com/repository/Volcengine/"
}
maven {
url uri("${rootDir}/repo")
}
}
}
1.3 引入依赖
工程中引入直播SDK的依赖,如下
implementation("com.bytedance.android:livesdk:xxx")
1.4 引入证书
直播SDK接入需要的播放器证书及安全证书拷贝到assets 目录下(相关证书文件,在判断您符合接入条件后,将
统一
申请提供)。
1.5 配置APPLOG_SCHEME
主工程build.gradle中添加APPLOG_SCHEME配置:
defaultConfig {
manifestPlaceholders.put("APPLOG_SCHEME", "APPLOG_SCHEME参数".toLowerCase())
}
1.6 abiFilters 配置
主工程build.gradle中添加abiFilters配置:
defaultConfig {
ndk {
abiFilters "armeabi-v7a" // 可支持v7a v8a(32位和64位)
}
}
1.7 编译配置
SDK开发是基于support28(AndroidX只支持到1.0.0,暂不支持更高版本),没有做 AndroidX 有关处理,
因此:
- AndroidX 项目需要在
gradle.properties
增加android.enableJetifier=true
- 新项目需要确认 Android Gradle Plugin 的版本 < 4.2.0
1.8 编译验证
编译构建App,可以成功生成apk文件,说明成功集成了直播SDK。
恭喜你,到这里你已经完成了抖音直播SDK 的集成工作,接下来可以使用SDK 功能了!
二、SDK初始化
本节以Kotlin为例,如果是Java可用LiveContextV2.Builder()
2.1 调用Live.init 方法初始化SDK
val builder = LiveContext.Builder()
// 根据技术支持同学提供的参数信息,正确填写以下参数
builder.context = context // context上下文,注意需要是application
builder.aid = 0 // 直播服务appid
builder.generalAppId = "0" // 应用appid
builder.ttSDKAppId = "1" // 应用appid
builder.ttSDKCertAssetsPath = "p" // 播放器证书名字
// init方法四个参数,如果使用java初始化,后两个参数填null。
Live.init(builder.build(), ILiveInitCallback {
// ...
})
2.2 申请存储权限(在首次使用直播功能之前)
private val PERMISSIONS_STORAGE = arrayOf(
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"
)
private val REQUEST_EXTERNAL_STORAGE = 1
fun verifyStoragePermissions(activity: Activity) {
try {
//检测是否有写的权限
val permission = ActivityCompat.checkSelfPermission(
activity,
"android.permission.WRITE_EXTERNAL_STORAGE"
)
if (permission != PackageManager.PERMISSION_GRANTED) {
// 若无写权限,则申请权限,会弹出对话框
ActivityCompat.requestPermissions(
activity,
PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
2.3 抖音账号授权
使用抖音直播SDK看播,必须进行抖音登陆授权。整体授权逻辑如下图所示
(其中方块内为开发者实现逻辑):

2.3.1 集成抖音开发平台SDK
根据抖音开放平台-Android 接入指南集成抖音开放平台SDK,完成抖音授权操作。
这一步完成后需要获取到如下授权信息:
- accessToken: 授权Token
- expireAt:Token失效时间点毫秒
- name:用户名,可为空
- openId:授权OpenId
授权信息
可以保存到SharePreference或者本地数据库中,直播SDK 使用时直接从存储地获取即可。
2.3.2 直播注入授权信息
直播中注入授权信息,需要开发者自己实现 IHostTokenInjectionAuth
接口和
LiveContextBuilder.IUserIdGetter
接口
- 实现
IHostTokenInjectionAuth
接口
interface IHostTokenInjectionAuth : IService {
/**
* true:可以正常进入直播间。
* false:进入直播间被拦截,测试时可以先默认返回true。
*
* 注意:根据业务合作合规需求要求,不允许匿名看播,因此要求宿主
* 必须在进入直播间之前确保用户的登陆态。返回值必须与接入App登录态绑定。
*
**/
fun isLogin():Boolean
/**
* 返回非null:
* a. TokenInfo 信息合法:直播正常使用。
* b. TokenInfo 信息不合法:TokenInfo信息校验不通过,下面方法`onTokenInvalid`
* 将会被自动回调。直播中账号相关互动操作无法执行。
*
* 返回null:方法`onTokenInvalid`将会被自动回调。
*
* 注意:TokenInfo expireAt参数,为在具体时间点过期,单位(ms)
* 抖音授权返回的为 expireIn, 过期时间多长,单位(s)
* 这里需要转化:expireAt = expireIn * 1000 + System.currentTime()
**/
fun getTokenInfo(): TokenInfo?
/**
* 上一个方法`getTokenInfo`返回的授权信息服务端校验不通过时调用。
*
* param tokenInfo: 校验不通过的授权信息
* param callback: 授权状态变更回调,成功调用onSuccess并传入token信息,失败调用onFailed并传
入一个Throwable携带错误信息
* param activity: 触发授权的环境的Activity,若由网络请求错误触发则为null
* param extra: 触发授权的环境的额外参数,若由网络请求错误触发则为null,若由用户交互行为触发
KEY_IS_INTERACTION 这个key将有值 true
**/
fun onTokenInvalid(tokenInfo: TokenInfo?, callback: TokenRefreshCallback, activity: Activity?,
extra: Map<String, String>?)
}
例子参考:
下面例子代码仅作为说明示意使用,无法编译通过,如果希望能直接copy到工程中完成授权,请复制文档末尾附件中提供的代码并完成后续步骤
class LiveHostTokenInjectAuth() : IHostTokenInjectionAuth {
override fun getTokenInfo(): TokenInfo? {
// 授权后获取到的token信息,或者用户未曾授权则返回null
return TokenHelper.getToken()
}
override fun isLogin(): Boolean {
// 在app内的登陆态,如果未登陆需要返回false
return true
}
override fun onTokenInvalid(
tokenInfo: TokenInfo?,
callback: TokenRefreshCallback,
activity: Activity?,ext:Map<String,String>?
) {
if (tokenInfo != null) {
if (tokenInfo.accessToken.isEmpty()) {
// 未授权用户允许了授权
TokenHelper.fetchToken({ token ->
// token获取成功 调用success告知直播sdk
callback.onSuccess(token)
},{ error ->
// token获取失败 调用onError告知直播sdk
callback.onError(error)
})
} else {
// 刷新token
TokenHelper.refetchToken({ token ->
// token刷新成功 调用success告知直播sdk
callback.onSuccess(token)
},{ error ->
// token刷新失败 调用onError告知直播sdk
callback.onError(error)
})
}
} else {
// 通常这个情况不需要处理,属于用户不允许授权的情况。
}
}
}
实现LiveContextBuilder.IUserIdGetter接口
该接口用于让直播SDK获取openId
interface IUserIdGetter {
// 返回当前已知登陆用户的OpenId,如果这个用户未授权过,返回空字符串 ""
override fun getUserId(): String
}
2.3.3 初始化时添加授权配置
初始化直播SDK时,添加授权配置。
val builder = LiveContext.Builder()
// 其他参数
builder.context = context
builder.aid = 0
//====== 授权相关配置 begin ========
// LiveHostTokenInjectByDouYin 为 `IHostTokenInjectionAuth`接口实现类。
builder.auth = LiveHostTokenInjectByDouYin()
// UserIdGetter 为 `LiveContextBuilder.IUserIdGetter` 接口的实现类
builder.userIdGetter = UserIdGetter()
//====== 授权相关配置 end ========
Live.init(builder.build(), ILiveInitCallback { initialized.postValue(true) })
恭喜你,到这一步你已经完成了抖音账号的授权操作!
2.4 充值账号打通
2.4.1 初始化时添加财经配置
添加完成后,检查钱包以及支付方式是否可以正常调起
val builder = LiveContext.Builder()
// .. 其他初始化参数
builder.cjAppId = "0" // 财经appId
builder.cjMerchantId = "0" // 财经商户号
Live.init(builder.build(), ILiveInitCallback { initialized.postValue(true) })
2.5 穿山甲客户配置(非必须)
如您已经接入穿山甲或准备接入穿山甲(广告等),需同时传入以下四个参数,请联系技术支持获得:
(如您有其他广告参与直播内容混排,请联系客服)
Partner
PartnerSecret
OriginPartner (非必填参数)
OriginUUID (非必填参数)
如果对接后,确认要使用上述字段,请使用如下接口传入:
val builder = LiveContext.Builder()
// Partner
builder.partner = "partner"
// PartnerSecret
builder.partnerSecret = "partnerSecret"
builder.partnerExtra = PartnerExtra("OriginPartner","OriginUUID")
// ...
Live.init(builder.build(), ILiveInitCallback { initialized.postValue(true) })
2.6 基本信息配置
基本信息不影响基础的功能使用,默认也从context中获取,影响包括信息展示、问题追踪、数
据查询等功能。若有特殊的信息定制需求,请自行复写。
// 初始化直播间SDK 的builder中添加App基础信息
val builder = LiveContext.Builder()
// App名称,用来展示在直播间内需要展示名称的地方
builder.appName = "LiveToB"
// App icon 展示在直播间内需要展示icon的地方
builder.appIcon = 0;
// App版本名称,用来在数据平台和反馈平台定位问题
builder.version = "nice"
// App版本号,用来在数据平台和反馈平台定位问题
builder.versionCode = 0
Live.init(builder.build(), ILiveInitCallback { initialized.postValue(true) })
三、自测验证
接入直播SDK后,强力建议参照下面case验证核心功能是否正常。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
四、常见问题
调试开关
val builder = LiveContextBuilder()
// 公共参数会携带此参数,设置为local_test将导致启用部分debug环境功能(含部分服务功能)
builder.channel = "local_test"
// 启动代码中的debug开关
builder.debug = false
Live.init(builder.build(), ILiveInitCallback { initialized.postValue(true) })
注:线上版本请务必关闭。
4.1 编译类问题
问题 | 解决方式 |
报错信息和直播SDK包含重复资源定义有关 Duplicate resources | 设置android.disableResourceValidation=true |
报错AndroidX相关问题 | 直播SDK本身没有禁用AndroidX, android.enableJetifier=true设置后如果还有问题, 请按照“其他编译问题” 处理 |
找不到抖音授权SDK的依赖/找不到字节侧 一些库依赖 | 根据文档检查maven引入是否正确 |
release编译不过,错误信息和proguard有关 | 尝试android.enableR8=false来关闭R8检查是否和R8有关。 以及尝试开启-dontoptimize 来检查是否和优化器有关。 按照“其他编译问题”处理,并将上述尝试的结果一起提交给字节侧。 |
其他编译问题 | 请联系技术支持同学,并附上如下信息 1.使用命令行编译,并在最后添加 --stacktrace 例如:./gradlew :app:assemble --stacktrace 得到的编译错误日志。 2. ./gradlew :app:dependencies 得到的依赖树 |
4.2 功能性问题
遇到以下问题时,请附上问题APK及使用的repo,crash请携带完整crash信息,至工单处发起oncall(一个链接)。
如果是release包的话,请给出crash信息中,和直播有关的堆栈的mapping信息。
问题 | 解决方式 |
初始化直接crash,一个native的crash | 查看安全SDK的签名字符串是否正确配置,如果确认正确配置, 至工单处发起oncall |
初始化直接crash,其他crash | 根据crash信息自查,如果自查没有信息的话,可把相关crash 信息带上,至工单处发起oncall |
初始化没crash,但是初始化成功没有走 | 至工单处发起oncall |
直播广场入口,无法进入直播间,无错误提示。 | 自行实现IHostAction并作为初始化参数(Builder中同样支持 此参数),并在startLive方法中调用 DefaultLivePlayerActivity.start(context, roomId, bundle)开启 直播,并try catch此调用。 如果调用未发生,至工单处发起oncall。 如果调用发生且发生crash,请根据crash信息自行调整。 如果调用发生且未发生crash,请检查授权实现中isLogin是否返回true。 |
直播广场入口,无法进入直播间,有错误提示, 进入直播间,然后显示错误提示切换到下个直 播间,几次切换后退出。 | 打印依赖树,检查rangersapplog版本是否是530. 直播尚未兼容高于此版本的rangersapplog |
直播广场入口,无法进入直播间,有错误提示, 其他。 | 联络技术支持查看,并将 1.错误信息截图 2.初始化直播代码(Builder的设置/LiveContext的实现) 至工单处发起oncall |
直播广场入口,可以进入直播间,进入后直接 crash,feasco相关 | 检查支持的so库版本,直播默认支持armeabi v7a及之上, 不支持v5 only。如果有特殊需求请联络相关技术支持。 |
直播广场入口,可以进入直播间,进入后 直接crash,发现Lottie有关的crash问题, 堆栈来源于一个Lottie NPE。 | 检查Lottie版本,直播只支持2.6.1的Lottie版本 |
直播广场入口,可以进入直播间,进入后 不展示流画面, 一直loading | 联系技术支持查看,播放器存在鉴权问题,查看播放器id 和证书是否正确放置。可以过滤ttmn看是否出现 auth failed错误,其他情况请保存日志后至工单处发起oncall。 |
直播广场入口,可以进入直播间,进入后 展示流画面,但是一段事件后卡住 | 至工单处发起oncall,播放器存在问题 |
直播广场入口,可以进入直播间,进入后 展示流画面,但是尺寸/比例异常 | 至工单处发起oncall,附上主播名称 |
流正常,无法上下滑动切换直播间 | 进房参数错误,至工单处发起oncall。 |
流正常,无更多直播入口 | 直播初始化错误,至工单处发起oncall。 |
PK直播间无PK血条 | 至工单处发起oncall,播放器存在鉴权问题,查看播放器id 和证书是否正确放置。可以过滤ttmn看是否出现auth failed 错误,其他情况请保存日志。 |
无法授权,任何操作都爆无授权错误 或重复弹出授权UI | 授权接口实现有问题,请在getTokenInfo方法返回之前打印 其返回值,并在onTokenInvalid方法开始打印调用堆栈, 并在调用回调的onSuccess和onFailed前,打印填充的参数信息。 将包含这些打印信息的包附上,至工单处发起oncall。 |
可授权,但是无法互动,弹出错误信息 | 如果弹出的错误信息是未授权,则按照“无法授权”处理。 其他错误至工单处发起oncall。 |
可授权,但是无法互动,也不弹出错误信息 | 至工单处发起oncall。 |
聊天区,自己发出聊天信息会出现两条 | 应用是否在直播间外切换了授权用户?如果有,需要实现 IHostUser接口以告知直播SDK发生了用户切换。内部接 入方自行在IBaseHostService中替换IHostUser实现,外 部接入方,请关注Live.init的第四个参数,并提供一个自 定义的IBaseHostService的实现,将其中IHostUser的实 现替换。 如果已经做了上述操作仍存在问题,请检查 onTokenInvalid被回调,授权完成后,回调了callback的 onSuccess并传入了正确的token。 若问题仍然存在,则按照“无法授权”处理。 |
关注后下滑再返回当前直播间,关注 状态被重置 | 账号被风控,至工单处发起oncall。附上当前账号token。 |
五、附件
分别把一下两个代码块中的内容复制到两个类文件中使用
package com.example.livesaasdemo.auth
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import com.bytedance.android.live.GsonHelper
import com.bytedance.android.livehostapi.platform.IHostTokenInjectionAuth
import com.bytedance.android.livehostapi.platform.TokenInfo
import com.bytedance.android.livehostapi.platform.TokenRefreshCallback
import com.bytedance.android.livesdkapi.util.url.UrlBuilder
import com.bytedance.sdk.open.aweme.authorize.model.Authorization
import com.bytedance.sdk.open.douyin.DouYinOpenApiFactory
import com.bytedance.sdk.open.douyin.DouYinOpenConfig
import com.google.gson.JsonObject
import com.google.gson.annotations.SerializedName
import okhttp3.*
import java.io.IOException
class AuthAccessToken(
@SerializedName("refresh_token") val refreshToken: String?,
@SerializedName("scope") val scope: String?,
@SerializedName("access_token") val accessToken: String?,
@SerializedName("description") val description: String,
@SerializedName("error_code") val errorCode: Long?,
@SerializedName("expires_in") val expiresIn: Long?, // 单位s
@SerializedName("open_id") val openId: String?,
@SerializedName("refresh_expires_in") val refreshExpiresIn: Long?
)
class AuthAccessTokenResponse(
@SerializedName("data") val data: JsonObject?,
@SerializedName("message") val message: String?
)
class RefreshToken(
@SerializedName("description") val description: String?,
@SerializedName("error_code") val errorCode: Long?,
@SerializedName("expires_in") val expiresIn: Long?,
@SerializedName("refresh_token") val refreshToken: String?,
@SerializedName("open_id") val openId: String?,
@SerializedName("refresh_expires_in") val refreshExpiresIn: Long?,
@SerializedName("scope") val scope: String?,
@SerializedName("access_token") val accessToken: String?
)
class RefreshTokenResponse(
@SerializedName("data") val data: JsonObject?,
@SerializedName("message") val message: String?
)
class AuthTool(
private val clientKey: String,
private val clientSecret: String,
private val host: String,
private val context: Context
) : IHostTokenInjectionAuth {
companion object {
lateinit var currentAuthTool: AuthTool
}
init {
currentAuthTool = this
}
private val KEY_TOKEN = "KEY_TOKEN"
private val KEY_REFRESH_TOKEN = "KEY_REFRESH_TOKEN"
private val KEY_OPEN_ID = "KEY_OPEN_ID"
private val KEY_EXPIRE_AT = "KEY_EXPIRE_AT"
private val GET_ACCESS_TOKEN_PATH = "https://${host}/oauth/access_token/"
private val REFRESH_TOKEN_PATH = "https://${host}/oauth/refresh_token/"
private var mMainHandler: Handler? = null
fun getMainHandler(): Handler {
if (mMainHandler == null) {
mMainHandler = Handler(Looper.getMainLooper());
}
return mMainHandler!!
}
object LiveNetworkClient {
private val client: OkHttpClient = OkHttpClient()
fun doGet(url: String, callback: Callback) {
val request = Request.Builder()
.url(url)
.build()
return client.newCall(request).enqueue(callback)
}
}
private var currentTokenInfo: TokenInfo? = null
private val tokenSp = context.getSharedPreferences("token_douyin_demo", Context.MODE_PRIVATE)
override fun getTokenInfo(): TokenInfo? {
if (currentTokenInfo == null) {
currentTokenInfo = getSavedTokenInfo()
}
return currentTokenInfo
}
override fun isLogin(): Boolean = true
override fun onTokenInvalid(
tokenInfo: TokenInfo?,
callback: TokenRefreshCallback,
activity: Activity?,
extra: Map<String, String>?
) {
if (extra == null) { // 需要刷新token
if (tokenInfo == null) {
// 第一次进入,无需刷新
} else {
refreshToken(tokenInfo, callback)
}
} else {
if (tokenInfo != null && tokenInfo.accessToken.isEmpty()) {
// 用户同意了授权
activity?.let {
fetchToken(it, callback)
}
} else if (extra["KEY_IS_INTERACTION"]?.equals("true") == true) {
// 用户开始没同意授权,但是后来的互动触发了授权
activity?.let {
fetchToken(it, callback)
}
} else {
// 奇怪的授权来源,请在此打印调用栈,找直播侧RD追查
}
}
}
private fun fetchToken(activity: Activity, callback: TokenRefreshCallback) {
requestAuth(activity, object : AuthCallback {
override fun onAuth(token: Token) {
currentTokenInfo = getSavedTokenInfo()
callback.onSuccess(currentTokenInfo!!)
}
override fun onCancel() {
callback.onFailed(IllegalStateException("cancel"))
}
})
}
private fun refreshToken(tokenInfo: TokenInfo, callback: TokenRefreshCallback) {
refreshToken(tokenSp.getString(KEY_REFRESH_TOKEN, null)!!, object : AuthCallback {
override fun onAuth(token: Token) {
currentTokenInfo = getSavedTokenInfo()
callback.onSuccess(currentTokenInfo!!)
}
override fun onCancel() {
callback.onFailed(IllegalStateException("cancel"))
}
})
}
fun saveToken(token: Token) {
tokenSp.edit().putLong(KEY_EXPIRE_AT,token.expireAt)
.putString(KEY_OPEN_ID,token.openId)
.putString(KEY_TOKEN,token.accessToken)
.putString(KEY_REFRESH_TOKEN,token.refreshToken).commit()
}
private fun getSavedTokenInfo(): TokenInfo? {
val token = tokenSp.getString(KEY_TOKEN, null)
return if (token == null) {
null
} else {
TokenInfo(
"name", tokenSp.getString(KEY_OPEN_ID, null) ?: "",
tokenSp.getString(KEY_TOKEN, null) ?: "",
tokenSp.getLong(KEY_EXPIRE_AT, 0)
)
}
}
private var currentAuthCallback: AuthCallback? = null
private fun requestAuth(activity: Activity, authCallback: AuthCallback) {
currentAuthCallback = authCallback
// step1. 通过账号sdk获取auth_code
val clientKey = clientKey
if (clientKey.isEmpty()) {
Toast.makeText(
context, "没有填写clientKey", Toast.LENGTH_SHORT
).show()
}
DouYinOpenApiFactory.init(DouYinOpenConfig(clientKey))
val douyinOpenApi = DouYinOpenApiFactory.create(activity)
val request = Authorization.Request()
request.scope = "user_info"
request.state = "ww" //用于保持请求和回调的状态,授权请求后原样带回给第三方。
request.callerLocalEntry = DouyinAccountActivity::class.java.name // 设置授权
的activity,可用于接收授权回调
douyinOpenApi.authorize(request)
}
private fun refreshToken(refreshToken: String, authCallback: AuthCallback) {
currentAuthCallback = authCallback
refreshAccessToken(refreshToken)
}
fun onAuthCodeUpdate(authCode: String? = null, throwable: Throwable? = null) {
if (authCode?.isNotEmpty() == true) {
getAccessToken(authCode)
} else {
currentAuthCallback?.onCancel()
}
}
private fun getAccessToken(authCode: String) {
// 该接口建议放到服务端请求
val urlBuilder = UrlBuilder(GET_ACCESS_TOKEN_PATH)
urlBuilder.addParam("client_key", clientKey)
urlBuilder.addParam("client_secret", clientSecret)
urlBuilder.addParam("code", authCode)
urlBuilder.addParam("grant_type", "authorization_code")
LiveNetworkClient.doGet(
urlBuilder.build(),
object : Callback {
override fun onFailure(call: Call, e: IOException) {
currentAuthCallback?.onCancel()
resetCallback()
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
response.body()?.let {
val accessTokenResponse =
GsonHelper.get()
.fromJson(it.charStream(), AuthAccessTokenResponse::class.java)
val data: JsonObject? = accessTokenResponse.data
data?.let { data ->
val accessTokenData: AuthAccessToken? =
GsonHelper.get().fromJson(data, AuthAccessToken::class.java)
when {
accessTokenData?.errorCode != 0L -> {
getMainHandler()
.post {
Toast.makeText(
context,
"接口错误:${accessTokenData?.description},
错误码:${accessTokenData?.errorCode}",
Toast.LENGTH_SHORT
).show()
}
currentAuthCallback?.onCancel()
}
accessTokenData.accessToken?.isNotEmpty() == true -> {
// step4:获取到access_token,调用直播接口更新token
val newToken =
Token(
accessTokenData.accessToken,
accessTokenData.openId,
convertExpiresAt(accessTokenData.expiresIn),
accessTokenData.refreshToken
)
saveToken(newToken)
currentAuthCallback?.onAuth(newToken)
}
else -> currentAuthCallback?.onCancel()
}
}
}
} else {
currentAuthCallback?.onCancel()
}
resetCallback()
}
})
}
private fun refreshAccessToken(oldRefreshToken: String) {
val urlBuilder = UrlBuilder(REFRESH_TOKEN_PATH)
urlBuilder.addParam("client_key", clientKey)
urlBuilder.addParam("grant_type", "refresh_token")
urlBuilder.addParam("refresh_token", oldRefreshToken)
LiveNetworkClient.doGet(
urlBuilder.build(),
object : Callback {
override fun onFailure(call: Call, e: IOException) {
currentAuthCallback?.onCancel()
resetCallback()
}
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
response.body()?.let {
val refreshTokenResponse: RefreshTokenResponse = GsonHelper.get()
.fromJson(it.charStream(), RefreshTokenResponse::class.java)
val data = refreshTokenResponse.data
data?.let { data ->
val refreshTokenData: RefreshToken? =
GsonHelper.get().fromJson(data, RefreshToken::class.java)
when {
refreshTokenData?.errorCode != 0L -> {
getMainHandler().post {
Toast.makeText(
context,
"接口错误:${refreshTokenData?.description},
错误码:${refreshTokenData?.errorCode}",
Toast.LENGTH_SHORT
).show()
}
Log.e(
AuthTool::class.java.simpleName,
"onResponse: ${refreshTokenData?.description},
错误码:${refreshTokenData?.errorCode}"
)
currentAuthCallback?.onCancel()
}
refreshTokenData.accessToken?.isNotEmpty() == true -> {
val newToken =
Token(
refreshTokenData.accessToken,
refreshTokenData.openId,
convertExpiresAt(refreshTokenData.expiresIn),
refreshTokenData.refreshToken
)
saveToken(newToken)
currentAuthCallback?.onAuth(newToken)
}
else -> currentAuthCallback?.onCancel()
}
}
}
} else {
currentAuthCallback?.onCancel()
}
resetCallback()
}
})
}
private fun resetCallback() {
if (currentAuthCallback != null) {
currentAuthCallback = null
}
}
// s -> ms
private fun convertExpiresAt(expiresIn: Long?): Long {
return if (expiresIn != null) {
System.currentTimeMillis() + expiresIn * 1000
} else {
0L
}
}
interface AuthCallback {
fun onAuth(token: Token)
fun onCancel()
}
data class Token(
val accessToken: String?,
val openId: String?,
val expireAt: Long = 0,
val refreshToken: String? = ""
)
}
package com.example.livesaasdemo.auth
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.FragmentActivity
import com.bytedance.sdk.open.aweme.CommonConstants
import com.bytedance.sdk.open.aweme.authorize.model.Authorization
import com.bytedance.sdk.open.aweme.common.handler.IApiEventHandler
import com.bytedance.sdk.open.aweme.common.model.BaseReq
import com.bytedance.sdk.open.aweme.common.model.BaseResp
import com.bytedance.sdk.open.douyin.DouYinOpenApiFactory
import com.bytedance.sdk.open.douyin.api.DouYinOpenApi
/**
* Created on 2021/2/5.
*/
class DouyinAccountActivity : FragmentActivity(), IApiEventHandler {
companion object {
const val TAG = "DouyinAccountActivity"
}
private lateinit var douYinOpenApi: DouYinOpenApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
douYinOpenApi = DouYinOpenApiFactory.create(this)
douYinOpenApi.handleIntent(intent, this)
}
override fun onResp(resp: BaseResp?) {
if (resp?.type == CommonConstants.ModeType.SEND_AUTH_RESPONSE) {
val response = resp as Authorization.Response
Log.e(
DouyinAccountActivity::class.java.simpleName,
"onResp: isSuccess=${response.isSuccess}, auth code is ${response.authCode}"
)
when {
response.isSuccess -> {
Log.d(TAG, "授权成功,获得权限:" + response.grantedPermissions)
// step3. 用auth_code换取access_token
AuthTool.currentAuthTool.onAuthCodeUpdate(response.authCode)
}
response.isCancel -> {
AuthTool.currentAuthTool.onAuthCodeUpdate(
null,
IllegalStateException("user cancel")
)
Log.d(TAG, "取消授权" + response.grantedPermissions)
}
else -> {
AuthTool.currentAuthTool.onAuthCodeUpdate(
null,
IllegalStateException("unknown error")
)
Log.e(TAG, "授权失败" + response.errorMsg)
}
}
finish()
}
}
override fun onErrorIntent(intent: Intent?) {
AuthTool.currentAuthTool.onAuthCodeUpdate(null, IllegalStateException("error intent"))
}
override fun onReq(req: BaseReq?) {
}
}