==================
动态注册Boardcast
protected void onResume() {
super.onResume();
try {
IntentFilter filter = new IntentFilter();
filter.addAction(ZenLoginModel.ZEN_LOGIN_FINISHED);
filter.addAction(ZenLoginModel.ZEN_LOGIN_FAILED);
registerReceiver(mBoradcastReceiver, filter);
} catch (Exception e) {
e.printStackTrace();
}
}
private BroadcastReceiver mBoradcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mLoading.hide();
String action = (String) intent.getAction();
if (action.equals(ZenLoginModel.ZEN_LOGIN_FINISHED)) {
ZenMyBoardsModel boardModel = ZenMyBoardsModel.getInstance();
boardModel.load(); //加载板块
ZenNotificationModel.getInstance().load();
AppMsg appMsg = AppMsg.makeText(ZenLoginActivity.this, "登录成功",
AppMsg.STYLE_INFO);
appMsg.show();
Handler handler = new Handler(getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 2000);
//...
}
}
注意,需要在onpause/onDestory中unregister:
unregisterReceiver(mBoradcastReceiver);
在ZenLoginModel处理完登录动作后
mContext.sendBroadcast(new Intent(ZEN_LOGIN_FINISHED));
broadcastReceiver 异步退出activity. broadcastReceiver / activity 不在同一个线程吗.
登录最终目的就是得到登录账号的cookie.代码中保存为ZenUtils.gettoken().如果已经登录过,会保存在SharedPreferences中.否则从web获取,代码:
public void OnResponse(String response) {
try {
if (response != null) {
System.out.println("response: " + response);
JSONObject json = new JSONObject(response);
JSONObject result = json.getJSONObject("result");
JSONObject user = result.getJSONObject("user");
userInfo.userName = user.getString("username");
userInfo.uid = user.getString("uid");
userInfo.token = user.getString("token");
isLogedin = true;
save();
fetchMSGToken();
mContext.sendBroadcast(new Intent(ZEN_LOGIN_FINISHED));
}
return;
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("zen login failed!");
mContext.sendBroadcast(new Intent(ZEN_LOGIN_FAILED));
}
=============
此model使用get方法获得板块数据.
示例url
example: http://mobileapi.hupu.com/1/1.1.1/bbs/getusercollectedboards?token=15538936|aWxvdHVv|1f4b|198a84db2223ce42d77a62d67a64517a|2223ce42d77a62d6
第一次接触web客户端,从域名可以看出原来网站为了拓展都预留一个api用于查询数据库?
连接,encoding(to utf-8),发送,getResponse,decoding(from utf-8),得到:
{“islogin”:1,”result”:[{“fid”:”151”,”cid”:”198”,”name”:”国家队-世界杯”},{“fid”:”63”,”cid”:”234”,”name”:”同城约战”},{“fid”:”34”,”cid”:”174”,”name”:”步行街”},{“fid”:”130”,”cid”:”1”,”name”:”篮球场”},{“fid”:”1048”,”cid”:”1”,”name”:”湿乎乎的话题”},{“fid”:”24”,”cid”:”232”,”name”:”中国篮球”},{“fid”:”2552”,”cid”:”1”,”name”:”深篮讨论区”},{“fid”:”43”,”cid”:”1”,”name”:”篮球图片”},{“fid”:”37”,”cid”:”1”,”name”:”NBA选秀-NCAA”},{“fid”:”1028”,”cid”:”1”,”name”:”NBA2K专区”},{“fid”:”3864”,”cid”:”57”,”name”:”极限运动X-GAMES”},{“fid”:”82”,”cid”:”1”,”name”:”凯尔特人区”},{“fid”:”3312”,”cid”:”82”,”name”:”凯尔特人生活区”},{“fid”:”85”,”cid”:”1”,”name”:”骑士专区”},{“fid”:”105”,”cid”:”1”,”name”:”马刺专区”},{“fid”:”3316”,”cid”:”105”,”name”:”马刺生活区”},{“fid”:”127”,”cid”:”1”,”name”:”快船专区”}]}
另外返回数据json使用utf-8编码.直接用在线解码会不成功.
如:
u80a5\u80a0\u714e\u86cb 肥肠煎蛋
应该使用 肥肠煎蛋 refer to link
Fiddler header :
User-Agent: Fiddler
Content-Type: application/json; charset=utf-8
Host: mobileapi.hupu.com
设计思路:
private ZenOnResponseListener mOnResponseListener :
ZenJSONUtil.WriteJSONToFile(ZenJSONUtil.ZEN_MY_BOARDS_JSON, jsonString);
ZenJSONUtil.reloadMyBoards();
ZenMenuFragment.java
mBoards.setOnChildClickListener(new OnChildClickListener() {
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
//切换到板块主题列表
switchContent(board.get("fid"), board.get("name"));
ZenThreadsFragment fragment = new ZenThreadsFragment();
//...
ac.switchContent(fragment, name);
}
}
以下的调用不一定在同一个函数中,只按照timeline排序. 缩进表示层次关系(可能跨多层)
ZenThreasFragment.java ZenThreadsModel.java
actureListView.setOnItemClickListener(...);
//获取主题列表数据 model init
mModel = new ZenThreadsModel(mContext, fid);
mModel.refresh();
load(mPage);//ZenThreadsModel.java
String url = String.format(ZenThreadsModel.ZenThreadsURL, mFid, page);
public void OnResponse(String response)
Intent intent = new Intent(ZenThreadsModel.DidFailedLoad);
mContext.sendBroadcast(intent);
public void onReceive(Context context, Intent intent) //BroadcastReceiver mBroadcastReceiver
mThreadsAdapter.array = mModel.threads;
//Adapter更新listView
mThreadsAdapter.notifyDataSetChanged();
mList.scrollTo(0, 0);
关于Adapter和listView的初始化见
ZenThreadsFragment.onActivityCreated
重点
actureListView.setOnItemClickListener(new OnItemClickListener()
Intent intent = new Intent(this, ZenContentActivity.class);
首先切换到另一个Activity了,具体网络通信模型和前面类似.
ZenReplyModel.java
下面使用这个 thread demo url 做例子:
http://mobileapi.hupu.com/1/1.1.1/bbs/getthreaddata?type=2&tid=12063126&boardpw=&token=15538936|aWxvdHVv|1f4b|198a84db2223ce42d77a62d67a64517a|2223ce42d77a62d6
return data:
{“islogin”:1,”result”:{“tid”:”12063126”,”fid”:”34”,”username”:”肥肠煎蛋”,”uid”:”20676879”,”subject”:”在火车上收一张图,发现自己的世界灰暗了”,”postdate”:”1425009205”,”lastpost”:”1425064033”,”lastposter”:”李时念”,”replies”:”693”,”locked”:”0”,”digest”:”0”,”lights”:”8”,”recs”:”15”,”content”:”
众jr看这张图什么颜色?我看的是淡紫加有点奇怪的绿色,但同学和下面的英文都告诉我,他们看得是白金或者蓝黑。
<%/center>
来自 Zen For Android <%/a>”,”boardname”:”步行街”,”boardurl”:”“,”url”:”“}}
此时webView只有帖子主题没回复,处理数据函数
private void parserThreadDataResponse(String response)完成了这些工作:
广播后最终在private void onThreadDataFinished(ZenThreadData data)处理主题数据后续.加载完主题数据,添加到webview后,开始加载回复数据,主要内容:
thread.loadUrl("javascript:clearPost('')"); //清除页面内容
String .format("javascript:addMainPost('%s', '%s', '%s', '%s', '%s', '%s');",
data.subject, postInfo, data.postdate,
data.author, "楼主", data.content);
//load it ..
mModel.loadReplies(mPage); //加载回复数据
加载了两条javascrpit语句.这些语句都在 hupu_post.html 模型中.
loadRelies 重复了上面类似的过程.加载json数据,调用javaScript语句,添加网页内容.完成后发送广播:
Intent successIntent = new Intent(ZenReplyModel.ZenRepliesDidFinishedLoad);
类似的,接收广播后处理下一步渲染:
private void onThreadRepliesFinished()
这一步开始收尾处理view:
pullToRefreshWebView.onRefreshComplete(); //webview complete loading.
mLoading.hide();
and finished reply post handling over javascript.
e.g.
thread.loadUrl("javascript:addLightTitle('热门跟帖', 'true')");
//...
ZenThreadReply reply = model.lightReplies.get(i);
String js = String
.format("javascript:addLightPost('%s', '%s', '%s', '%s', '%d', '%s')",
reply.author, "", reply.light, reply.content,
i, reply.pid);
thread.loadUrl(js);
总结
客户端是通过
thread.loadDataWithBaseURL(“file:///android_asset/”, html, “text/html”,
“utf-8”, null);
加载模板.再获得API json数据,结合javascript脚本生成帖子的.可以比较m.hupu.com 和zen生成的页面并不一样.如图:
借助端点工具查看各阶段的webView.
至于这个模板(assert/hupu_post.html)从何而来,我就不得而知了.
android Fragments详解四:管理fragment
下面仍然是ZenContentActivity相关的代码.
Menu 封装了所有使用这个MenuBar的按钮操作,使用了Adapter设计模式的,其中按钮相应函数:
public void OnMenuItemClick(int type) {
switch (type) {
case ZenMenuBar.MENU_LIGHT:
light();
break;
case ZenMenuBar.MENU_REPLY:
reply();
break;
case ZenMenuBar.MENU_COPY:
copy();
break;
case ZenMenuBar.MENU_PM:
pm();
break;
case ZenMenuBar.MENU_REFRESH:
refresh();
break;
case ZenMenuBar.MENU_COMMENT:
comment();
break;
case ZenMenuBar.MENU_RECOMMEND:
recommend();
break;
case ZenMenuBar.MENU_ARCHIVE:
archive();
break;
}
}
light,refresh操作和前面类似,加载这个文件:
String url = String.format(Locale.getDefault(), ZEN_LIGHT_URL,
mFid, mTid, pid, URLEncoder.encode(token, "utf-8"));
然后处理完response json后在 mBroadcastReceiver 调用javascript渲染:
mLoading.hide();
String lights = intent.getStringExtra("light");
String js = String.format(Locale.getDefault(),
"javascript:lightSuccess('%s', '%s', %d)", lights,
mReplyData.pid, mArea);
thread.loadUrl(js);
AppMsg appMsg = AppMsg.makeText(ZenContentActivity.this,
"点亮成功", AppMsg.STYLE_INFO);
appMsg.show();
recommend实现
推荐使用和web端一样的链接,但是他们的cookie是通用的:
private static final String ZEN_RECOMMEND_URL = "http://bbs.hupu.com/indexinfo/buddys.php";
String tokenEncoded = URLEncoder.encode(token, "utf-8");
mConnection = new ZenURLConnection(ZEN_RECOMMEND_URL);
mConnection.setOnResponseListener(mRecommendListener);
mConnection.setHttpMethod("POST");
mConnection.addRequestHeader("Content-Type",
"application/x-www-form-urlencoded ; charset=UTF-8");
mConnection.addRequestHeader("Cookie", "u=" + tokenEncoded + ";");
mConnection.addRequestHeader("X-Requested-With", "XMLHttpRequest");
mConnection.addRequestHeader("Referer", "http://bbs.hupu.com/"
+ mTid + ".html");
String httpBody = "fid=" + mFid + "&act=rc" + "&cid=" + mTid
+ "&title=" + URLEncoder.encode(title, "utf-8") + "&rmmsg="
+ URLEncoder.encode(content, "utf-8") + "&type=1";
mConnection.setHttpBody(httpBody);
mConnection.startAsychronous();
Comment/Reply实现
Comment实现比上面要复杂些.
上传图片:
String token = ZenUtils.getToken();
if (token != null) {
String boundary = "----pluploadboundary" + ZenUtils.timestamp();
mUploadConnection = new ZenURLConnection(ZEN_UPLOAD_URL);
mUploadConnection.setHttpMethod("POST");
mUploadConnection.addRequestHeader("Cookie",
"u=" + URLEncoder.encode(token, "utf-8"));
mUploadConnection.addRequestHeader("Content-Type",
"multipart/form-data; boundary=" + boundary);
InputStream body = boundary(image, boundary, isGif);
if (body != null) {
mUploadConnection.setHttpInputStream(body);
String response = mUploadConnection.startSychronous();
if (response != null) {
System.out.println("upload: " + response);
JSONObject json = new JSONObject(response);
if (json.has("pic")) {
String url = json.getString("pic");
return url;
}
}
}
}
注意boundary 是内容分割符.客户端往服务器上传内容需要靠分割符识别不同类型多媒体的request.参考rfc2616 session 19.2
.请求主体在boundary
中构造.请求成功后返回这样的一个key {pic:url} 的json数据reponse.
然后来看完整的comment过程是如何调用上面的上传函数:
ZenAssetsModel model = ZenAssetsModel.getInstance();
ArrayList
最后上传评论:
if (pid != null && !pid.equals("")) {
String response = oldcomment(content, pid, urls);
return response;
} else {
String response = newcomment(content, pid, title, urls);
newcomment 提交的header在前面都有用到过.cookie,refer,User-Agent.body部分含有复杂的信息构建了一段html段落.包括引用,作者,回复楼层,和”来自Zen”尾巴等等.部分代码:
contentBuf.append(content);
if (urls != null) {
for (String url : urls) {
contentBuf.append("
+ url
+ "\">
");
}
}
contentBuf.append(ZEN_TAIL);
//ZENTAIL 为content添加尾巴
private static final String ZEN_TAIL = "
来自 Zen For Android";
就写到这了.时间轴上这是最后停笔的地方.
我只是想接触下http协议.老早之前就大概明白一个互联网产品客户端是怎么写出来的了.后面的坑也不填了~反正也没人看.寒假本来只是想看两个客户端源码,接触了解下http,web服务器相关知识.作为非计算机专业科班出身的学生的一个知识补充.还买了http翻了一半.忽然惊醒还有几个月就要找实习了,才觉得补充过头了,竟然耗了整整一个寒假的时间.赶紧打住回到底层代码~为那个实习工作努力去!
关于javaScript调用ZenBridge函数
refer to :
Android addJavaScriptInterface
只需要添加以下代码
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
toggle();
break;
case R.id.zen_new_post:
即可实现如下效果:
zen.id.new_post is one item of a menu resource.
略
略
基础回顾–抽象类概念
public abstract class BaseExpandableListAdapter implements ExpandableListAdapter,
HeterogeneousExpandableList
implements 无需实现
ArrayList 也能这样遍历
public ArrayList topics;
for (ZenTopicData topic : topics)
//ArrayList 也能这样遍历
移除一个inflate 动态加载的View
/**
* 移除一个inflate 动态加载的View
* @param view
*/
private void removeFromSuperView(View view) {
ViewGroup superView = (ViewGroup) view.getParent();
if (superView != null) {
superView.removeView(view);
}
}
Instantiates a layout XML file into its corresponding
/*
* Instantiates a layout XML file into its corresponding {@link android.view.View}
* objects. It is never used directly. Instead, use
* {@link android.app.Activity#getLayoutInflater()} or
* {@link Context#getSystemService} to retrieve a standard LayoutInflater instance
* that is already hooked up to the current context and correctly configured
* for the device you are running on. For example:
*/
LayoutInflater inflater = (LayoutInflater)context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
动态生成View多使用FrameLayout
inflate.inflate(R.layout.zen_menu_bar_more, null);
xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/zen_menu_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
另一种oclick监听方法
<ImageButton
android:contentDescription="@null"
android:id="@+id/zen_back_btn"
android:onClick="OnBottomItemClick" />
java:
public void OnBottomItemClick(View v) {
switch (v.getId()) {
case R.id.zen_back_btn:
finish();
break;
case R.id.zen_prev_btn:
hint();
mLoading.show("正在加载...");
mModel.prev();
break;
// case etc..
}
快速写入应用私有文件
OutputStream output = context.openFileOutput(fileName, Context.MODE_PRIVATE);
这个开源版本中,有一些广告插件的代码.但是release中看不到广告.所以不做研究
导入工程
布局原理
挑选一些布局方案分析一下.首先是主题列表中独立的自定义View
效果图是这样的:
回复数量那里会有一个伪随机的颜色变化,实现多彩效果.
其实是这样弄出来的:
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:layout_marginTop="5dp" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/comment_box" />
<TextView
android:id="@+id/zen_thread_replies"
android:layout_width="24dp"
android:layout_height="16dp"
android:layout_gravity="top"
android:background="@drawable/mid_green"
android:gravity="center"
android:textColor="#fff"
android:textSize="11sp" />
FrameLayout>
ZenThreadsAdapter.java:
public View getView(int position, View convertView, ViewGroup parent) {
replies.setBackgroundResource(color);
//...
}
private int colorForReplies(int replyNum) {
int index = replyNum % colors.length;
return colors[index];
}
private int colors [] = {
R.drawable.orange,
R.drawable.navy,
R.drawable.real_blue,
R.drawable.purple,
R.drawable.green
};
原来是覆盖一个大小合适的textview在图片上面,用textView底色变色
[TODO]
编码问题
win7下默认unicode编码.返回的数据如下(example)\u672a\u77e5\u9519\u8bef解决:使用fidder设置header