抖音直播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接口

  1. 实现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验证核心功能是否正常。

  1. 点击视频Tab左上角直播按钮,进入直播间后无异常
  1. 进入直播间后上下滑动可正常切换直播间
  1. 各类直播间显示正常
  1. 在直播间内可进行双击点赞
  1. 发送一条评论,评论正常可见
  1. 评论消息多时可上下滑动
  1. 礼物面板可正常打开,显示礼物
  1. 送出礼物可正常显示礼物特效
  1. 点击更多按钮,显示举报、未成年退款、充值记录,相关功能正常
  1. 点击主播头像,成功吊起个人卡片且信息展示正确
  1. 点击个人卡片的关注,成功关注
  1. 点击在线观众可正常展示当前在线观众
  1. 点击小时榜正常展示小时榜主播,小时榜主播可正常关注、跳转直播间
  1. 点击更多直播其他直播间正常显示,点击后跳转正常
  1. 从礼物面板点开充值,在充值面板上选择测试套餐进行充值,充值完成后礼物面板抖币余额刷新

四、常见问题

调试开关

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?) {
    }
}

诚邀您对本文档易读易用性进行评价
好用
不好用