26
2017
09

三方操作功能模块封装(一),三方登录

最近自己封装一个三方登录和分享功能模块,现在算是写一篇总结吧。本片博文主要讲解 QQ 登录微信登录 的功能封装,如有错误,恳请斧正!

功能和要求

  • 实现三方登录和三方分享功能,封装成一个独立模块
  • 支持微信/QQ账号登陆
  • 支持分享到 微信好友/微信朋友圈/QQ好友/QQ空间
  • 接口统一, 简单易用
  • 易于扩展,日后方便接入其他平台

前期准备

  • 在 AS 新建一个module,填写 module 名称和最小 sdk。
    这里写图片描述
  • QQ登录和分享,获取 APP ID 和 APP KEY ,若未有腾讯开发者账号,需要先注册账号再获取,获取网址
  • 微信登录和分享,获取 APP ID 和 AppSecret,获取地址
  • 导入 sdk

    • 下载 QQ sdk,下载地址
      这里写图片描述

      1. 选择 Android 基础包,解压后可以看到基础包和全包说明文档,我们使用基础包即可。
        这里写图片描述

      2. 将 jar 包拷贝到项目 libs 目录下
        这里写图片描述

      3. 添加依赖,选择 File–>Project Structure –>Dependencies,点击右侧加号,选择 Jar dependency ,选中我们添加进来的 jar 包,选择 OK 。
        这里写图片描述 这里写图片描述

  • 获取微信 sdk,只需要在build.gradle文件中,添加如下依赖即可:
compile 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'
  • 接收微信的请求和返回值
    • 在你的包名相应目录下新建一个wxapi目录,并在该wxapi目录下新增一个WXEntryActivity类,该类继承自Activity(例如应用程序的包名为net.sourceforge.simcpux,则新添加的类如下图所示), 并在manifest文件里面加上exported属性,设置为true,如下所示:
      这里写图片描述 这里写图片描述
  • 实现IWXAPIEventHandler接口,微信发送的请求将回调到onReq方法,发送到微信请求的响应结果将回调到onResp方法,在WXEntryActivity中将接收到的intent及实现了IWXAPIEventHandler接口的对象传递给IWXAPI接口的handleIntent方法。
public class WXEntryActivity extends Activity implements IWXAPIEventHandler {

    private static final String TAG = "WXEntryActivity";

    // IWXAPI 是第三方app和微信通信的openapi接口
    private IWXAPI api;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        api = WXAPIFactory.createWXAPI(this, Constant.WX_APP_ID, false);

        //注意:
        //第三方开发者如果使用透明界面来实现WXEntryActivity,需要判断handleIntent的返回值,如果返回值为false,
        // 则说明入参不合法未被SDK处理,应finish当前透明界面,避免外部通过传递非法参数的Intent导致停留在透明界面,引起用户的疑惑
        try {
            boolean handleIntent = api.handleIntent(getIntent(), this);
            Log.i(TAG,"onCreate handleIntent:" + handleIntent);
            if(!handleIntent){
                finish();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

实现思路

这里写图片描述

这里写图片描述

QQ 登录

  1. QQ 登录结构如下所示
    这里写图片描述

  2. QQ 登录结构说明

    • 对于 ThirdPlatformLoginController 使用者来说,他们不关心登录走到哪一步骤,只需要知道调用登录接口方法和登录结果。
    • 在我们新建 module 新建ThirdPlatformLoginUtil 工具类,它只对外界提供登录接口,使用者无需知道登录操作细节。
    • 获取 Token 信息必须依赖 Activity,新建 UiControlActivity ,重写 onActivityResult( ) 方法,否则无法收到 QQ 回调信息。
    • ThirdPlatformLoginHandler 类处理回调 Token 信息,用来换取 QQ 个人信息,最后将登录结果回调通知 ThirdPlatformLoginController 使用。
  3. 创建 Platform 单例
    三方登录和分享操作都属于耗时操作,当我们发起某个请求时,需要等待 QQ 或者 微信的回调。等请求结果回来时,我们再通知调用方,一般使用 callback 方式,这也会带来内存泄露的风险,需要管理好自己 callback 的生命周期。还有,我们要怎样做到准确通知到具体调用方,即是哪一个callback ,这里使用单例来存储操作的 callback、分享数据 和 请求方式。

public class Platform {

    private DataListener dataListener;
    private int requestType;
    private Bundle bundle;

    private Platform(){

    }

    private static class Holder{
        private static final Platform platform = new Platform();
    }

    public DataListener getDataListener() {
        return dataListener;
    }

    public static Platform instance(){
        return Holder.platform;
    }

    public void setDataListener(DataListener dataListener) {
        this.dataListener = dataListener;
    }

    public int getRequestType() {
        return requestType;
    }

    public void setRequestType(int requestType) {
        this.requestType = requestType;
    }

    public Bundle getBundle() {
        return bundle;
    }

    public void setBundle(Bundle bundle) {
        this.bundle = bundle;
    }

    public void clearPlatformResource(){
        this.dataListener = null;
    }

}
  4. 创建 DataListener 接口

onDestroyActivity 用来 UiControlActivity 执行 onDestroy( ) 时通知使用者。正常情况下,我们在获取 QQ 登录结果后再 finish Activity ,但是在某些极端情况下,例如用户开启不保留活动、内存紧张时候,在发起登录请求后,UiControlActivity 就被系统回收,而登录结果还没回调。这不仅导致我们后续登录流程失败,还会造成内存泄露。所以我们可以在 UiControlActivity 被系统回收时回调执行释放资源等操作。

/** * 基本 callback */

public interface DataListener {

    void onComplete(Object response);

    void onError(Object response);

    void onCancel();

    void onDestroyActivity();

}
 5. 创建 BaseDataListener 抽象类
/** * 释放资源 callback */

public abstract class BaseDataListener implements DataListener {
    @Override
    public void onComplete(Object response) {
        clearResource();
    }

    @Override
    public void onError(Object response) {
        clearResource();
    }

    @Override
    public void onCancel() {
        clearResource();
    }

    @Override
    public void onDestroyActivity() {
        clearResource();
    }

    /** * 释放资源 */
    public void clearResource(){

    }
}
 6. ThirdPlatformLoginController 使用者

使用者 继承 BaseDataListener,重写抽象类方法。 调用 三方登录模块 ThirdPlatformLoginUtil 登录QQ方法,以入参形式把自己传给 ThirdPlatformLoginUtil 。

/** * QQ 登录 */
public void loginByQQ() {
    thirdPlatformLoginUtil.loginByQQ(this);
}
 7. 三方登录模块 ThirdPlatformLoginUtil

实例化 ThirdPlatformLoginHandler 对象,用于处理回调数据,存入 Platform 单例对象,使用 Intent 打开 UiControlActivity .

/** * QQ 登录 * @param loginListener 调用方信息回调 */
public void loginByQQ(BaseDataListener loginListener) {

    if(null == context){
        ThirdPlatformManager.Logger.i(TAG,"loginByQQ: context is null");
        return;
    }

    boolean checkAppPackage = CheckAppUtils.checkHasInstallPackage(context, CheckAppUtils.qq_package_name);
    if(checkAppPackage){
        Platform platform = Platform.instance();

        loginHandler = new ThirdPlatformLoginHandler(context,loginListener);
        platform.setDataListener(loginHandler);
        platform.setRequestType(Constant.QQ_LOGIN);

        Intent intent = new Intent();
        intent.setClass(context,UiControlActivity.class);
        context.startActivity(intent);
    }else {
        ThirdPlatformManager.Logger.i(TAG, "loginByWX: user do not install QQ");
        AlertUtil.toast(context, R.string.login_not_install_qq);
    }
}
 8. UiControlActivity

a. 实例化 Tencent 对象,调用QQ 登录接口

//实例化 Tencent 对象
mTencent = Tencent.createInstance(Constant.QQ_APP_ID,this);

//调用 QQ 登录接口
mTencent.login(this, "all", weakReferenceLoginListener);

b. 重写 onActivityResult 方法,否则无法收到 QQ 登录回调信息

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.i(TAG, "onActivityResult");
    super.onActivityResult(requestCode, resultCode, data);
    Tencent.onActivityResultData(requestCode,resultCode,data, weakReferenceLoginListener);
}

c. 改用弱引用
WeakReferenceLoginListener 实现 IUiListener 接口(Tencent 指定接口)。最开始直接使用官方文档推荐写法,传入 IUiListener 对象,登录操作完成后, LeakCanary 总是提醒内存泄漏,查看日志发现被 Tencent 类静态对象引用导致无法释放,Tencent 类提供 releaseResource()释放资源方法,开始以为是需要自己调用释放资源,后来发现调用了不起作用,进入源码发现竟然是空方法。
这里写图片描述

既然如此,那就把回调写成弱引用吧,代码如下,当成功获取 Token 后继续请求 QQ 个人信息,其他情况就回调通知调用者。

public class WeakReferenceLoginListener implements IUiListener {

    private static final String TAG = WeakReferenceLoginListener.class.getSimpleName();

    private final WeakReference<UiControlActivity> weakReference;

    public WeakReferenceLoginListener(UiControlActivity activity){
        weakReference = new WeakReference<>(activity);
    }

    @Override
    public void onComplete(Object object) {

        if(object != null){
            Log.i(TAG,"initListener loginListener doComplete" + object.toString());
            QQTokenModel model = parserQQToken(object.toString());
            //使用 Token 换 QQ 个人信息
            weakReference.get().updateUserInfo(model);
        }else {
            //获取 Token 信息为空
            weakReference.get().loginResponseIsNull();
        }
    }

    @Override
    public void onError(UiError uiError) {
        weakReference.get().notifyLoginError(uiError);
    }

    @Override
    public void onCancel() {
        weakReference.get().notifyLoginCancel();
    }

    private QQTokenModel parserQQToken(String data){
        Gson gson = new Gson();
        return gson.fromJson(data,QQTokenModel.class);
    }
}
 9. 使用 Token 换 QQ 个人信息

当执行 updateUserInfo( ) 方法,回调给 ThirdPlatformLoginHandler 执行 Token 换 QQ 个人信息操作,然后关闭界面。虽然也可以在 UiControlActivity 直接完成 Token 换 QQ 个人信息操作,但是这样处理有两个原因:
a. 使用 Token 换 QQ 个人信息不需要依赖界面;
b. 在 ThirdPlatformLoginHandler 执行 Token 换 QQ 个人信息操作可以避免 Activity 被回收导致回调失败风险。

public void updateUserInfo(QQTokenModel model) {

    if(dataListener != null){
        Log.i(TAG, "updateUserInfo onComplete model: " + model.toString());
        dataListener.onComplete(model);
    }

    finish();

}
 10. ThirdPlatformLoginHandler 收到 Ui 回调

根据请求类型调用获取 QQ 个人信息方法 getQQUserInfo ( )。UserInfo 是腾讯提供的用于获取 QQ 个人信息的类,实例化 UserInfo 对象需要传入 context 和 QQToken 对象,在这里需要注意的是,实例化 UserInfo 对象必须先更新 Tencent 对象 OpenId、AccessToken 和 expiresIn,否则导致获取个人信息失败;登录结果通过 DataListener 回调通知ThirdPlatformLoginController 登录调用者。

private void getQQUserInfo(QQTokenModel model) {

    //获取 QQ 个人信息 回调
    IUiListener getUserInfoListener = new IUiListener(){

        @Override
        public void onComplete(Object response) {
            if(response != null){
                Log.i(TAG,"getUserInfoListener onComplete" + response.toString());
                QQUserInfoModel userInfoModel = parserQQUserInfo(response.toString());

                if(listener != null){
                    listener.onComplete(userInfoModel);
                }
            }else {
                listener.onComplete(null);
            }
        }

        @Override
        public void onError(UiError uiError) {
            Log.i(TAG,"getUserInfoListener onError" + uiError.toString());

            if(listener != null){
                listener.onError(uiError);
            }
        }

        @Override
        public void onCancel() {
            Log.i(TAG,"getUserInfoListener onCancel");
            if(listener != null){
                listener.onCancel();
            }
        }
    };

Tencent mTencent = Tencent.createInstance(Constant.QQ_APP_ID,context);

//更新 Tencent 对象 OpenId、AccessToken 和 expiresIn
mTencent.setAccessToken(model.getAccess_token(), model.getExpires_in());
mTencent.setOpenId(model.getOpenid());

//UserInfo 是腾讯提供的类,用于获取 QQ 个人信息
UserInfo userInfo = new UserInfo(context,mTencent.getQQToken());
userInfo.getUserInfo(getUserInfoListener);
}
 11. ThirdPlatformLoginController 根据登录结果接入自身应用处理流程,例如成功 获取 QQ 个人信息,判断是否为新账号,若是新账号,走注册账户流程,否则走登录流程。

微信登录

  1. 微信登录结构如下
    这里写图片描述
  2. 微信登录结构说明
    • 微信登录和分享操作总体与 QQ 登录结构类似,但它不需要依赖界面,所以新建 WxHandleRequest 统一管理微信请求。
    • 微信处理请求后,会主动调起 WXEntryActivity onResp()方法。
  3. ThirdPlatformLoginUtil
    实例化 ThirdPlatformLoginHandler 对象,存入 Platform 单例对象,使用 WxHandleRequest 发送微信登录请求,获取微信临时票据凭证 code。
/** * 微信登录 * ThirdPlatformLoginHandler 处理 code 、 token 数据 */
public void loginByWX(BaseDataListener loginListener) {

    if(null == context || null == loginListener){
        ThirdPlatformManager.Logger.i(TAG,"loginByWX or loginListener context is null");
        return;
    }

    boolean checkAppPackage = CheckAppUtils.checkHasInstallPackage(context, CheckAppUtils.wc_package_name);
    if(checkAppPackage){

        loginHandler = new ThirdPlatformLoginHandler(context,loginListener);
        Platform platform = Platform.instance();

        platform.setDataListener(loginHandler);
        platform.setRequestType(Constant.WX_LOGIN);

        wxHandleRequest.sendWxHandleRequest();
    }else {
        Log.i(TAG, "loginByWX: user do not install wechat");
        AlertUtil.toast(context, R.string.login_not_install_wx);
    }
}
 4. WxHandleRequest

统一处理微信登录、分享的请求,不涉及数据操作。因为现在处理微信登录操作,我先不把微信分享请求放进来。

public void sendWxHandleRequest() {

    if(null == api){
        Log.i(TAG, "sendWxHandleRequest api is null");
        return;
    }
    //请求类型
    int type = Platform.instance().getRequestType();
    Log.i(TAG, "sendWxHandleRequest type: " + type);

    switch (type){ case Constant.WX_LOGIN: SendAuth.Req req_get_wx_token = WxRequestUtils.getWxCode(); api.sendReq(req_get_wx_token); break; }
}
 5. WxRequestUtils

微信请求内容工具类,buildTransaction( )是为每一个请求生成唯一标识。

private static final String GET_TOKEN_SCOPE = "snsapi_userinfo";

public static SendAuth.Req getWxCode(){
    SendAuth.Req req = new SendAuth.Req();
    req.scope = GET_TOKEN_SCOPE;
    req.state = buildTransaction("getWxCode");
    return req;
}
 6. 微信响应请求

微信收到第三方应用发送请求,将会通过 WXEntryActivity onResp( ) 方法响应。我们先处理微信回调数据,然后关闭界面。

// 第三方应用发送到微信的请求处理后的响应结果,会回调到该方法
@Override
public void onResp(BaseResp resp) {
    dealRespData(resp);
    finish();
}
 7. 处理微信回调数据

先在 Platform 单例对象获取回调 DataListener 对象,根据返回数据码 errCode 类型 回调给 ThirdPlatformLoginHandler。

private void dealRespData(BaseResp resp) {

    Platform platform = Platform.instance();
    DataListener dataListener = platform.getDataListener();
    if(dataListener == null) {
        Log.i(TAG,"dealRespData dataListener is null");
        return;
    }
    Log.i(TAG,"dealRespData resp.errCode: " + resp.errCode);
    switch (resp.errCode) {
        case BaseResp.ErrCode.ERR_OK:
            dataListener.onComplete(resp);
            break;
        case BaseResp.ErrCode.ERR_USER_CANCEL:
            dataListener.onCancel();
            break;
        default:
            dataListener.onError(resp);
            break;
    }
}
 8.  ThirdPlatformLoginHandler 收到获取 微信临时票据凭证 code 结果

若获取失败,则直接回调给使用者 ThirdPlatformLoginController 。若获取成功,则进行第二步,通过code获取access_token。这里大家可以使用自己项目网络请求框架获取微信 Token 信息,我引用最简单 Volley 框架发起一个 Get 请求(微信文档要求使用 Get 方法,虽然我试过 Post 也能获取到 Token 信息)。若获取成功继续下一步,通过 token 换取 微信个人信息。

private void requestWxToken(Object response) {

    SendAuth.Resp resp = (SendAuth.Resp) response;
    String code = resp.code;
    String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + Constant.WX_APP_ID
            + "&secret=" + Constant.AppSecret + "&code=" + code + "&grant_type=authorization_code";

    //使用 code 换取 token
    RequestQueue requestQueue = Volley.newRequestQueue(context);
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            Log.i(TAG,"onResponse response: " + response.toString());

            //解析用户 token 信息
            TokenModel tokenModel = parseTokenData(response);

            //获取个人信息
            requestWxUserInfo(tokenModel);

        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Log.i(TAG,"onErrorResponse error" + error.toString());
            listener.onError(error);
        }
    });
    requestQueue.add(request);
}
 9. 通过 token 换取 微信个人信息,将获取结果回调通知 ThirdPlatformLoginController 使用者
private void requestWxUserInfo(TokenModel tokenModel) {

    String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + tokenModel.getAccess_token()+
            "&openid=" + tokenModel.getOpenid();

    RequestQueue requestQueue = Volley.newRequestQueue(context);
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            Log.i(TAG,"onResponse response: " + response.toString());
            WxUserInfo wxUserInfo = parserWxUserInfo(response);
            listener.onComplete(wxUserInfo);

        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            Log.i(TAG,"onErrorResponse error" + error.toString());
            listener.onError(error);
        }
    });
    requestQueue.add(request);
}
 10. ThirdPlatformLoginController 使用者收到微信登录结果,接入自身应用处理流程。

实现思路

  1. 三方登录和分享操作无法保证一定成功,有可能是自身代码质量导致,有时也可能是用户自身原因,例如网络问题、没有安装微信客户端,客户端版本太低等等。为了方便查找问题,我们通过日志记录同时,还可以选择性暴露一些用户原因导致的问题,这样在收到用户反馈时候能够更加快速定位到问题原因。
  2. QQ 公共返回码说明地址微信返回码说明地址
  3. 将错误码分类,例如分为可提醒用户和不提醒用户,纯属体力活呐
    这里写图片描述

  4. 新建 ErrorHandler 类,统一处理三方操作错误。
    这里写图片描述

  5. 因为三方操作都会回调到 ThirdPlatformLoginHandler ,我们可以在 onError( ) 方法做错误信息处理。
    这里写图片描述

上一篇:访问Google的两种方法 下一篇:java之环境变量