本次更新的内容主要是增加点赞和反对以及回复别人评论的三个功能。主要的思路就是先抓包获得观网app的相关接口,然后再封装到自己的app里面,实现和观网官方的app相同的功能。
一、抓包
首先打开抓包工具Fiddler4
然后打开手机上的观网官方app,随便点一下点赞反对,然后回复一下别人评论,然后再Fiddler4中找到相对应的网络请求:
上图中三个绿色背景的网络请求即对应于点赞,反对,评论别人回复三个动作。下面分别来分析三个网络请求:
1、点赞的网络请求
选择上面的点赞网络请求,然后请求头这边点raw(即原始数据),响应头这边点击json:
请求头这边:网络请求的类型为GET,接口为https://app.guancha.cn/comment/praise,后面有个叫做access-token的参数,经过前面的登录环节的分析,我猜测这个token是与设备相关的一个唯一识别码,用于标识网络请求的用户和设备。然后cookie被设置为一个防csrf网络攻击字符串。后面提交的表单,字段分别为comment_id和from,这个comment_id,根据我前面的分析,为一个评论的识别码,知道comment_id之后可以拿到这个评论的全部信息(根据这个comment_id来看,观网的所有评论加起来已经有1300多万条了)。后面的from字段在手机客户端上是固定的,值都为cms,意思就是来自手机客户端。
响应头这边,返回的json字符串中的msg字段,显示为“成功”两个字,可以将这个字段定为点赞是否成功的标志。
2、反对的网络请求
同样的操作还是打开点击反对的网络请求,往后请求头选择raw(原始数据),响应头选择json:
将此网络请求的格式与前面点赞的网络请求对比,可以发现他们的格式是一样的,只是接口名称,和防csrf网络攻击字符串改掉了。反对的接口为https://app.guancha.cn/comment/tread,后面的请求头,不叫comment_id,而直接叫id。
3、回复别人评论的网络请求
点开第三个绿色背景网络请求,请求头点raw,相应头点json:
这一个网络请求明显比前面两个内容要多一些。
首先请求头这边的表单内容有好几项,为了看的更直观,可以点击webform(表单)这一项:
可以很清楚的看到,这一个网络请求所提交的表单,有6个字段。其中parent_id字段一看就是所回复的评论的comment_id,from字段和上面一样,代表着来自手机客户端,type目前不清楚代表什么,统一写成1应该不会有什么问题,access_device应该是设备类型,Android客户端为3,code_id根据以前的分析,为文章的唯一标识符。拿到code_id就可以拿到这篇文章的所有信息,比如所有评论等等。最后的content就是我刚才所发表的评论内容。
响应头这里,和前面的点赞反对一样,根据msg这个字段,可以判断是否回复成功。
网络分析部分到此结束,下面是写代码实现这些功能。
二、写代码实现功能
思路:写一个接口,为这些图标添加相应的事件监听器。
打开Android studio:
先看一下设计的评论布局:
如上图所示,监听的对象主要是两个点赞图标,两个反对图标,还有两个评论(至于分享和举报,先搁置)。
打开界面上RecyclerView的Adapter,为了保持简洁性(虽然目前的代码已经挺混乱了),这里选择创建三个方法,然后将相应的功能封装到这三个方法中,最后在相关控件的事件监听器中调用:
//点赞的回调函数
private void praise(ImageView praiseIcon,int commentId){
}
//反对的回调函数
private void trample(ImageView praiseIcon,int commentId){
}
//回复评论的回调函数
private void commentOthers(TextView praiseIcon,int commentId){
}
1、点赞功能的代码实现
创建两个全局字符串变量和一个OKHttpClient对象,用来存放token,和csrf字符串和进行网络请求:
private String csrfState,token;
OkHttpClient client = new OkHttpClient();
然后初始化这两个变量,因为后面的功能还要用到,因此将这个初始化过程封装到一个方法中:
private void initSessionProperty(){
List<UserProperty> properties = LitePal.findAll(UserProperty.class);
if (properties.size() == 0) {
return;
}
//拿到本地的csrf数据
List<SessionProperty> session = LitePal.findAll(SessionProperty.class);
if (session.size() != 0) csrfState = session.get(0).getCsrfState();
//下面拿到用户的token令牌,目测token与设备相关
UserProperty userProperty = properties.get(0);
token = userProperty.getUserToken();
}
将设备相关的token字符串和接口连接起来:
initSessionProperty();
if (token == null) return;
String praiseUrl = "https://app.guancha.cn/comment/praise?access-token=" + token;
创建需要提交到服务器的表单:
FormBody praiseForm = new FormBody.Builder()
.add("comment_id",String.valueOf(commentId))
.add("from","cms")
.build();
创建网络请求:
//创建Request
Request praiseRequest = new Request.Builder()
.url(praiseUrl)
.header("Cookie",csrfState)
.post(praiseForm)
.build();
进行网络请求,添加网络响应的监听器,判断是否操作成功:
//向观网服务器提交请求,并且监听返回结果
Call praiseCall = client.newCall(praiseRequest);
praiseCall.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//提示用户操作失败
context.runOnUiThread(() -> Toast.makeText(context,"点赞失败!",Toast.LENGTH_LONG).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//拿到返回的字符串
String praiseRespond = response.body().string();
try{
//封装成json对象
JSONObject praiseJson = new JSONObject(praiseRespond);
//判断是否点赞成功
if (praiseJson.getString("msg").equals("成功")){
//将图标换成已经点赞的图标
praiseIcon.setImageResource(R.drawable.ic_zan_ed);
//提示用户点赞成功
context.runOnUiThread(() -> Toast.makeText(context,"点赞成功!",Toast.LENGTH_LONG).show());
}else {
//提示用户点赞失败
context.runOnUiThread(() -> Toast.makeText(context,"点赞失败!",Toast.LENGTH_LONG).show());
}
//更新csrf字符串
SessionProperty singleProperty = new SessionProperty();
String newCsrf = response.header("Set-Cookie","");
if (!newCsrf.equals("")){
csrfState = newCsrf;
singleProperty.setCsrfState(newCsrf);
singleProperty.updateAll();
}
}catch(Exception e){
e.printStackTrace();
}
}
});
在onBindViewHolder方法里面,将方法的调用写入点赞按钮的事件监听器:
commentViewHolder.praisedIcon.setOnClickListener(v -> praise(commentViewHolder.praisedIcon,comment.getCommentId()));
下面调试,看效果:
2、反对功能的代码实现
通过上面对网络请求的分析,可以发现,反对和点赞的形式是一样的,改个参数就行了,所以这里就把praise方法中的代码直接复制到反对的方法中,改一下url就可以了,直接贴修改后的代码:
//初始化环境,主要是拿到token和csrf字符串
initSessionProperty();
//如果每户没登陆,则直接返回
if (token == null) return;
praiseIcon.setClickable(false);
//创建接口字符串
String praiseUrl = "https://app.guancha.cn/comment/tread?access-token=" + token;
//创建表单
FormBody praiseForm = new FormBody.Builder()
.add("id",String.valueOf(commentId))
.add("from","cms")
.build();
//创建Request
Request praiseRequest = new Request.Builder()
.url(praiseUrl)
.header("Cookie",csrfState)
.post(praiseForm)
.build();
//向观网服务器提交请求,并且监听返回结果
Call praiseCall = client.newCall(praiseRequest);
praiseCall.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//提示用户操作失败
context.runOnUiThread(() -> Toast.makeText(context,"反对失败!",Toast.LENGTH_LONG).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//拿到返回的字符串
String praiseRespond = response.body().string();
Log.d(TAG, "onResponse: 返回的字符串:" + praiseRespond);
try{
//封装成json对象
JSONObject praiseJson = new JSONObject(praiseRespond);
//判断是否点赞成功
if (praiseJson.getString("msg").equals("成功")){
context.runOnUiThread(() -> {
//将图标换成已经点赞的图标
praiseIcon.setImageResource(R.drawable.ic_zan_ed);
//提示用户点赞成功
Toast.makeText(context,"反对成功!",Toast.LENGTH_LONG).show();
});
}else {
//提示用户点赞失败
context.runOnUiThread(() -> Toast.makeText(context,"反对失败!",Toast.LENGTH_LONG).show());
}
//更新csrf字符串
SessionProperty singleProperty = new SessionProperty();
String newCsrf = response.header("Set-Cookie","");
if (!newCsrf.equals("")){
csrfState = newCsrf;
singleProperty.setCsrfState(newCsrf);
singleProperty.updateAll();
}
}catch(Exception e){
e.printStackTrace();
}
}
});
同样在onBindViewHolder方法里面,将方法的调用写入反对按钮的事件监听器:
commentViewHolder.dislikeIcon.setOnClickListener(v -> trample(commentViewHolder.dislikeIcon,comment.getCommentId()));//执行传入的监听器中的反对方法
然后跑一下测试:
3、回复别人评论功能的代码实现
这一段有些复杂,因为要调用输入法输入评论,与主界面的评论输入框进行一些交互,所以要在Activity中写一些调用输入法的代码。
首先在适配器中写一个监听器内部类,用于Activity和Adapter的交互:
public interface OnCommentClickListener{
void onCommentClick(int commentId);
}
然后在适配器内部添加一个监听器全局变量,并且添加一个方法,可以在外部初始化这个监听器:
//评论被点击的监听器
private OnCommentClickListener clickListener;
//初始化上面的clickListener
public void setOnCommentClickListener(OnCommentClickListener listener){
this.clickListener = listener;
}
然后在commentOthers方法里面,调用监听器方法,然后在onBindViewHolder方法里面设置comment的监听事件(可以直接在监听事件里面调用监听器方法,我这是为了简洁一些):
//回复评论的回调函数
private void commentOthers(TextView praiseIcon,int commentId){
if (clickListener != null){
clickListener.onCommentClick(commentId);
}
}
commentViewHolder.comment.setOnClickListener(v -> commentOthers(commentViewHolder.comment,comment.getCommentId()));//执行传入的监听器中的评论方法
然后在Activity中,添加一个commentId的全局变量,给Adapter添加一个监听器,主要是让输入评论的EditText获得焦点,并拉起输入法:
//添加一个Adapter的监听器,用于回复别人的评论
CommentAdapter.OnCommentClickListener clickListener = id -> {
//将当前的commentId设置为传入的id
currentcommentId = id;
//使输入框获得焦点
commentText.requestFocus();
//设置提示的文字
commentText.setHint("回复他的评论");
//弹出输入法
((InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(commentText,0);
};
然后复用以前的评论方法addCommentButtonListener,不过需要做一些修改,首先把方法签名改为带一个int类型:
private void addCommentButtonListener(int CommentId){
...
}
然后将方法中网络请求的表单parent_id这一项改为前面传进来的id:
FormBody form = new FormBody.Builder()
.add("access_device","3")
.add("parant_id",String.valueOf(commentId))
.add("code_id",codeId)
.add("type","1")
.add("content",commentText.getText().toString())
.add("from","cms")
.build();
然后就可以测试了,看效果:
OK,本次更新完成,最后贴一下本App 在GitHub上的链接: