public class FavoritesAPI {

	private static final String BASE_URI = CatnutAPI.API_DOMAIN + "/2/favorites";

	/**
	 * 添加一条微博到收藏里
	 *
	 * @param id 要收藏的微博ID
	 * @return api
	 */
	public static CatnutAPI create(long id) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("/create.json").append("?id=").append(id);
		return new CatnutAPI(Request.Method.POST, uri, true, null);
	}

	/**
	 * 要取消收藏的微博ID
	 *
	 * @param id 要收藏的微博ID
	 * @return api
	 */
	public static CatnutAPI destroy(long id) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("/destroy.json").append("?id=").append(id);
		return new CatnutAPI(Request.Method.POST, uri, true, null);
	}

	/**
	 * 获取当前登录用户的收藏列表
	 *
	 * @param count 单页返回的记录条数，默认为50
	 * @param page  返回结果的页码，默认为1
	 * @return api
	 */
	public static CatnutAPI favorites(int count, int page) {
		StringBuilder uri = new StringBuilder(BASE_URI).append(".json");
		uri.append("?count=").append(CatnutUtils.optValue(count, 50))
				.append("&page=").append(CatnutUtils.optValue(page, 1));
		return new CatnutAPI(Request.Method.GET, uri, true, null);
	}
} public class CatnutApp extends Application {

	public static final String TAG = "CatnutApp";

	/** singleton */
	private static CatnutApp sApp;
	/** http request header for weibo' s access token */
	private static Map<String, String> sAuthHeaders;

	private RequestQueue mRequestQueue;
	private SharedPreferences mPreferences;
	private AccessToken mAccessToken;

	@Override
	public void onCreate() {
		super.onCreate();
		sApp = this;
		mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
		mRequestQueue = Volley.newRequestQueue(this, new OkHttpStack());
		checkAccessToken();
	}

	/** 检查用户是否已经成功授权，并且授权是否过期 */
	private void checkAccessToken() {
		if (mAccessToken == null) {
			mAccessToken = getAccessToken();
		}

		if (mAccessToken != null) {
			long now = System.currentTimeMillis();
			if (now > mAccessToken.expires_in) {
				// 保存用户的uid信息意义不大，因为只是清掉了uid，他所保存的其它信息依然存在
				Log.d(TAG, "用户授权已过期，清除授权信息...");
				invalidateAccessToken();
			} else {
				Log.d(TAG, "授权将在" + DateUtils.getRelativeTimeSpanString(mAccessToken.expires_in) + "过期！");
			}
		}
	}

	public SharedPreferences getPreferences() {
		return mPreferences;
	}

	/**
	 * 保存授权用户的授权信息
	 *
	 * @param json 包含access token的json对象
	 */
	public void saveAccessToken(JSONObject json) {
		Log.d(TAG, "save access token...");
		mPreferences.edit()
				.putLong(UID, json.optLong(UID))
				.putString(ACCESS_TOKEN, json.optString(ACCESS_TOKEN))
				.putLong(EXPIRES_IN, json.optLong(EXPIRES_IN) * 1000 + System.currentTimeMillis())
				.commit();
	}

	/**
	 * 注销用户
	 */
	public void invalidateAccessToken() {
		Log.d(TAG, "invalidate access token...");
		mPreferences.edit()
				.remove(UID)
				.remove(EXPIRES_IN)
				.remove(ACCESS_TOKEN)
				.commit();
		mAccessToken = null;
		if (sAuthHeaders != null) {
			sAuthHeaders.clear();
		}
		sAuthHeaders = null;
	}

	/**
	 * 获取已经成功授权用户的信息
	 *
	 * @return AccessToken
	 */
	public AccessToken getAccessToken() {
		if (mAccessToken == null) {
			long uid = mPreferences.getLong(UID, 0L);
			long expiresIn = mPreferences.getLong(EXPIRES_IN, 0L);
			String accessToken = mPreferences.getString(ACCESS_TOKEN, null);
			// 当且仅当三项都完备的时候我们才会初始化授权信息对象！
			if (uid != 0L && expiresIn != 0L && accessToken != null) {
				mAccessToken = new AccessToken(uid, expiresIn, accessToken);
			}
		}
		return mAccessToken;
	}

	/**
	 * 获取异步http请求队列对象
	 *
	 * @return RequestQueue
	 */
	public RequestQueue getRequestQueue() {
		return mRequestQueue;
	}

	/**
	 * 获得boolean偏好
	 *
	 * @param key
	 * @param defaultValue
	 * @return a boolean with default res bool value
	 */
	public static boolean getBoolean(int key, int defaultValue) {
		SharedPreferences pref = sApp.getPreferences();
		Resources res = sApp.getResources();
		return sApp.getPreferences().getBoolean(sApp.getString(key), res.getBoolean(defaultValue));
	}

	/**
	 * 直接以静态方法获取应用程序对象
	 *
	 * @return CatnutApp
	 */
	public static CatnutApp getTingtingApp() {
		return sApp;
	}

	/**
	 * 获得在请求头部设置access token的map，避免每次都在uri中append上access token
	 * <p/>
	 * 没有授权则返回空的map
	 *
	 * @return headers with access token
	 */
	public static Map<String, String> getAuthHeaders() {
		// 如果没有授权，返回null
		AccessToken accessToken = sApp.getAccessToken();
		if (accessToken == null) {
			return Collections.emptyMap();
		}

		if (sAuthHeaders == null) {
			sAuthHeaders = new HashMap<String, String>(1);
			sAuthHeaders.put("Authorization", "OAuth2 " + accessToken.access_token);
		}
		return sAuthHeaders;
	}
} public class CatnutRequest extends Request<JSONObject> {

	public static final String TAG = "CatnutRequest";

	protected Context mContext;
	protected CatnutAPI mApi;
	protected CatnutProcessor<JSONObject> mProcessor;
	protected Response.Listener<JSONObject> mListener;

	public CatnutRequest(
		Context context,
		CatnutAPI api,
		CatnutProcessor<JSONObject> processor,
		Response.Listener<JSONObject> listener,
		Response.ErrorListener errorListener) {
		super(api.method, api.uri, errorListener);
		mContext = context;
		mApi = api;
		mProcessor = processor;
		mListener = listener;
	}

	@Override
	protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
		try {
			String jsonString =
					new String(response.data, HttpHeaderParser.parseCharset(response.headers));
			Response<JSONObject> success = Response.success(new JSONObject(jsonString),
					HttpHeaderParser.parseCacheHeaders(response));
			if (mProcessor != null) {
				// do in background...
				mProcessor.asyncProcess(mContext, success.result);
			}
			return success;
		} catch (UnsupportedEncodingException e) {
			return Response.error(new ParseError(e));
		} catch (JSONException je) {
			return Response.error(new ParseError(je));
		} catch (Exception ex) {
			Log.e(TAG, "process error!", ex);
			return Response.error(new VolleyError(response));
		}
	}

	@Override
	protected void deliverResponse(JSONObject response) {
		if (mListener == null) {
			Log.d(TAG, "finish http request without response on main-thread!");
		} else {
			mListener.onResponse(response);
		}
	}

	@Override
	public Map<String, String> getHeaders() throws AuthFailureError {
		return mApi.authRequired ? CatnutApp.getAuthHeaders() : super.getHeaders();
	}

	@Override
	protected Map<String, String> getParams() throws AuthFailureError {
		return mApi.params == null ? super.getParams() : mApi.params;
	}
} public class GalleryPagerFragment extends Fragment {
	public static final String TAG = GalleryPagerFragment.class.getSimpleName();

	public static final String CUR_INDEX = "cur_index";
	public static final String URLS = "urls";
	public static final String TITLE = "title";

	private List<Uri> mUrls;
	private int mCurrentIndex;

	public static GalleryPagerFragment getFragment(int currentIndex, ArrayList<Uri> urls, String actionBarTitle) {
		Bundle args = new Bundle();
		args.putInt(CUR_INDEX, currentIndex);
		args.putParcelableArrayList(URLS, urls);
		args.putString(TITLE, actionBarTitle);
		GalleryPagerFragment fragment = new GalleryPagerFragment();
		fragment.setArguments(args);
		return fragment;
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		mUrls = getArguments().getParcelableArrayList(URLS);
		mCurrentIndex = getArguments().getInt(CUR_INDEX);
	}

	@Override
	public void onStart() {
		super.onStart();
		getActivity().getActionBar().setTitle(getArguments().getString(TITLE));
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		return inflater.inflate(R.layout.pager, container, false);
	}

	@Override
	public void onViewCreated(View view, Bundle savedInstanceState) {
		ViewPager viewPager = (ViewPager) view;
		viewPager.setAdapter(new FragmentStatePagerAdapter(getFragmentManager()) {
			@Override
			public Fragment getItem(int position) {
				return SimpleImageFragment.getFragment(mUrls.get(position));
			}

			@Override
			public int getCount() {
				return mUrls.size();
			}
		});
		viewPager.setPageTransformer(true, new PageTransformer.ZoomOutPageTransformer());
		viewPager.setCurrentItem(mCurrentIndex);
	}

	public static class SimpleImageFragment extends Fragment {

		public static SimpleImageFragment getFragment(Uri url) {
			Bundle args = new Bundle();
			args.putParcelable(Constants.PIC, url);
			SimpleImageFragment fragment = new SimpleImageFragment();
			fragment.setArguments(args);
			return fragment;
		}

		@Override
		public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
			View view = inflater.inflate(R.layout.photo, container, false);
			view.setBackgroundColor(getResources().getColor(R.color.black_background));
			TouchImageView image = (TouchImageView) view.findViewById(R.id.image);
			Picasso.with(getActivity())
					.load((Uri) getArguments().getParcelable(Constants.PIC))
					.error(R.drawable.error)
					.into(image);
			return view;
		}
	}
} public final class Status implements CatnutMetadata<JSONObject, ContentValues> {

	private static final String TAG = "Status";

	/** 标记微博的类型，本地使用 */ // todo: 可能一条微博有多个type...
	public static final String TYPE = "_type";
	/** 评论指向的微博id，本地使用 */
	public static final String TO_WHICH_TWEET = "_to";

	/** 本地使用，标记为主页微博 */
	public static final int HOME = 2;
	/** 本地使用，标记为转发微博 */
	public static final int RETWEET = 3;
	/** 本地使用，标记为公共微博 */
	public static final int PUBLIC = 4;
	/** 本地使用，标记为评论 */
	public static final int COMMENT = 5;
	/** 本地使用，标记为@我的微博 */
	public static final int MENTION = 6;
	/** 本地使用，标记为其它类型的微博 */
	public static final int OTHERS = 1;


	public static final String TABLE = "statuses";
	public static final String SINGLE = "status";
	public static final String MULTIPLE = "statuses";

	public static final String COMMENTS = "comments";
	public static final String FAVORITES = "favorites";
	public static final String total_number = "total_number";

	public static final String MEDIUM_THUMBNAIL = "bmiddle";
	public static final String LARGE_THUMBNAIL = "large";

	// singleton
	public static final Status METADATA = new Status();

	private Status() {
	}

	// 自关联，直接存储json字符串
	public static final String retweeted_status = "retweeted_status";

	/** 创建时间 */
	public static final String created_at = "created_at";
	/** 内容 */
	public static final String text = "text";
	public static final String columnText = "_text"; // keywords...
	/** 来源 */
	public static final String source = "source";
	/** 是否已收藏 boolean */
	public static final String favorited = "favorited";
	/** 是否被截断 boolean */
	public static final String truncated = "truncated";
	/** （暂未支持）回复ID */
//	public static final String in_reply_to_status_id = "in_reply_to_status_id";
	/** （暂未支持）回复人UID */
//	public static final String in_reply_to_user_id = "in_reply_to_user_id";
	/** （暂未支持）回复人昵称 */
//	public static final String in_reply_to_screen_name = "in_reply_to_screen_name";
	/** 微博配图地址，key=thumbnail_pic，多图时返回多图链接，无配图返回"[]" array */
	public static final String pic_urls = "pic_urls";
	/** 缩略图片地址，没有时不返回此字段 */
	public static final String thumbnail_pic = "thumbnail_pic";
	/** 中等尺寸图片地址，没有时不返回此字段 */
	public static final String bmiddle_pic = "bmiddle_pic";
	/** 原始图片地址，没有时不返回此字段 */
	public static final String original_pic = "original_pic";
	/** 被转发的原微博信息字段，当该微博为转发微博时返回 long */
	public static final String retweeted_status_id = "retweeted_status_id";
	// todo: geo
	/** 作者 long */
	public static final String uid = "uid";
	/** 转发数 int */
	public static final String reposts_count = "reposts_count";
	/** 评论数 int */
	public static final String comments_count = "comments_count";
	/** 表态数 int */
	public static final String attitudes_count = "attitudes_count";
	/** 暂未支持 */
	public static final String mlevel = "mlevel";
	// todo: visible
	// 微博的可见性及指定可见分组信息。该object中type取值，0：普通微博，1：私密微博，3：指定分组微博，4：密友微博；list_id为分组的组号
	// "visible": {
	// 	"type": 0,
	//	"list_id": 0
	// }

	@Override
	public String ddl() {
		Log.i(TAG, "create table [statuses]...");

		StringBuilder ddl = new StringBuilder("CREATE TABLE ");
		ddl.append(TABLE).append("(")
				.append(BaseColumns._ID).append(" int primary key,")
				.append(TYPE).append(" int,") // 标记微博的类型
				.append(TO_WHICH_TWEET).append(" int,") // 评论指向的微博id
				.append(created_at).append(" text,")
				.append(columnText).append(" text,")
				.append(source).append(" text,")
				.append(favorited).append(" int,")
				.append(truncated).append(" int,")
				.append(pic_urls).append(" text,")
				.append(thumbnail_pic).append(" text,")
				.append(bmiddle_pic).append(" text,")
				.append(original_pic).append(" text,")
				.append(retweeted_status).append(" text,")
				.append(uid).append(" int,")
				.append(reposts_count).append(" int,")
				.append(comments_count).append(" int,")
				.append(attitudes_count).append(" int");
		return ddl.append(")").toString();
	}

	@Override
	public ContentValues convert(JSONObject json) {
		ContentValues tweet = new ContentValues();
		tweet.put(BaseColumns._ID, json.optLong(Constants.ID));
		tweet.put(created_at, json.optString(created_at));
		// 注意json字段和sql字段的不同！
		tweet.put(columnText, json.optString(text));
		tweet.put(source, json.optString(source));
		tweet.put(favorited, json.optBoolean(favorited));
		tweet.put(truncated, json.optBoolean(truncated));
		// 有些时候，数据来源是不可靠的
		JSONArray thumbs = json.optJSONArray(pic_urls);
		if (thumbs != null) {
			// 直接存储为json字符串
			tweet.put(pic_urls, thumbs.toString());
		}
		tweet.put(thumbnail_pic, json.optString(thumbnail_pic));
		tweet.put(bmiddle_pic, json.optString(bmiddle_pic));
		tweet.put(original_pic, json.optString(original_pic));
		// 检查是否是转发微博
		if (json.has(retweeted_status)) {
			tweet.put(retweeted_status, json.optJSONObject(retweeted_status).toString());
		}
		// 加入作者的外键
		if (json.has(User.SINGLE)) {
			tweet.put(uid, json.optJSONObject(User.SINGLE).optLong(Constants.ID));
		} else if (json.has(uid)) {
			tweet.put(uid, json.optLong(uid));
		}
		tweet.put(reposts_count, json.optInt(reposts_count));
		tweet.put(comments_count, json.optInt(comments_count));
		tweet.put(attitudes_count, json.optInt(attitudes_count));
		return tweet;
	}
} public final class User implements CatnutMetadata<JSONObject, ContentValues> {

	private static final String TAG = "User";

	public static final String TABLE = "users";
	public static final String SINGLE = "user";
	public static final String MULTIPLE = "users";

	public static final String next_cursor = "next_cursor";
	public static final String total_number = "total_number";

	// singleton
	public static final User METADATA = new User();

	private User() {
	}

	/** 昵称 */
	public static final String screen_name = "screen_name";
	/** 友好显示名称 */
	public static final String name = "name";
	/** 所在省级ID int */
	public static final String province = "province";
	/** 所在省级ID int */
	public static final String city = "city";
	/** 所在地 */
	public static final String location = "location";
	/** 个人描述 */
	public static final String description = "description";
	/** 博客地址 */
	public static final String url = "url";
	/** 头像地址（中图），50×50像素 */
	public static final String profile_image_url = "profile_image_url";
	/** 主页封面图片（桌面） */
	public static final String cover_image = "cover_image";
	/** 主页封面图片（手机） */
	public static final String cover_image_phone = "cover_image_phone";
	/** 微博统一URL地址 */
	public static final String profile_url = "profile_url";
	/** 个性化域名 */
	public static final String domain = "domain";
	/** 微号 */
	public static final String weihao = "weihao";
	/** 性别，m：男、f：女、n：未知 */
	public static final String gender = "gender";
	/** 粉丝数 int */
	public static final String followers_count = "followers_count";
	/** 关注数 int */
	public static final String friends_count = "friends_count";
	/** 微博数 int */
	public static final String statuses_count = "statuses_count";
	/** 收藏数 int */
	public static final String favourites_count = "favourites_count";
	/** 用户创建（注册）时间 */
	public static final String created_at = "created_at";
	/** 暂未支持 boolean */
	public static final String following = "following";
	/** 是否允许所有人给我发私信 boolean */
	public static final String allow_all_act_msg = "allow_all_act_msg";
	/** 是否允许标识用户的地理位置 boolean */
	public static final String geo_enabled = "geo_enabled";
	/** 是否是微博认证用户，即加V用户 boolean */
	public static final String verified = "verified";
	/** 暂未支持 int */
	public static final String verified_type = "verified_type";
	/** 用户备注信息，只有在查询用户关系时才返回此字段 */
	public static final String remark = "remark";
	/** int */
//	public static final String ptype = "ptype";
	/** 是否允许所有人对我的微博进行评论 boolean */
	public static final String allow_all_comment = "allow_all_comment";
	/** 头像地址（大图），180×180像素 */
	public static final String avatar_large = "avatar_large";
	/** 头像地址（高清），高清头像原图 */
	public static final String avatar_hd = "avatar_hd";
	/** 认证原因 */
	public static final String verified_reason = "verified_reason";
	/** 是否关注当前登录用户 boolean */
	public static final String follow_me = "follow_me";
	/** 在线状态，0：不在线、1：在线 int */
	public static final String online_status = "online_status";
	/** 互粉数 int */
	public static final String bi_followers_count = "bi_followers_count";
	/** 当前的语言版本，zh-cn：简体中文，zh-tw：繁体中文，en：英语 */
	public static final String lang = "lang";
	/** int */
//	public static final String star = "star";
	/** int */
//	public static final String mbtype = "mbtype";
	/** int */
//	public static final String mbrank = "mbrank";
	/** int */
//	public static final String block_word = "block_word";
	/** 关联最新的一条微博 long */
	public static final String status_id = "status_id";

	@Override
	public String ddl() {
		Log.i(TAG, "create table [users]...");

		StringBuilder ddl = new StringBuilder("CREATE TABLE ");
		ddl.append(TABLE).append("(")
				.append(BaseColumns._ID).append(" int primary key,")
				.append(screen_name).append(" text,")
				.append(name).append(" text,")
				.append(province).append(" int,")
				.append(city).append(" int,")
				.append(location).append(" text,")
				.append(description).append(" text,")
				.append(url).append(" text,")
				.append(profile_image_url).append(" text,")
				.append(cover_image).append(" text,")
				.append(cover_image_phone).append(" text,")
				.append(profile_url).append(" text,")
				.append(domain).append(" text,")
				.append(weihao).append(" text,")
				.append(gender).append(" text,")
				.append(followers_count).append(" int,")
				.append(friends_count).append(" int,")
				.append(statuses_count).append(" int,")
				.append(favourites_count).append(" int,")
				.append(created_at).append(" text,")
				.append(following).append(" int,")
				.append(allow_all_act_msg).append(" int,")
				.append(geo_enabled).append(" int,")
				.append(verified).append(" int,")
				.append(verified_type).append(" int,")
				.append(remark).append(" int,")
//				.append(ptype).append(" int,")
				.append(allow_all_comment).append(" int,")
				.append(avatar_large).append(" text,")
				.append(avatar_hd).append(" text,")
				.append(verified_reason).append(" text,")
				.append(follow_me).append(" int,")
				.append(online_status).append(" int,")
				.append(bi_followers_count).append(" int,")
				.append(lang).append(" text"); // 注意最后一个字段！
//				.append(star).append(" int,")
//				.append(mbtype).append(" int,")
//				.append(mbrank).append(" int,")
//				.append(block_word).append(" int");
		return ddl.append(")").toString();
	}

	@Override
	public ContentValues convert(JSONObject json) {
		ContentValues user = new ContentValues();
		user.put(BaseColumns._ID, json.optLong(Constants.ID));
		user.put(screen_name, json.optString(screen_name));
		user.put(name, json.optString(name));
		user.put(province, json.optInt(province));
		user.put(city, json.optInt(city));
		user.put(location, json.optString(location));
		user.put(description, json.optString(description));
		user.put(url, json.optString(url));
		user.put(profile_image_url, json.optString(profile_image_url));
		user.put(cover_image, json.optString(cover_image));
		user.put(cover_image_phone, json.optString(cover_image_phone));
		user.put(profile_url, json.optString(profile_url));
		user.put(domain, json.optString(domain));
		user.put(weihao, json.optString(weihao));
		user.put(gender, json.optString(gender));
		user.put(followers_count, json.optInt(followers_count));
		user.put(friends_count, json.optInt(friends_count));
		user.put(statuses_count, json.optInt(statuses_count));
		user.put(favourites_count, json.optInt(favourites_count));
		user.put(created_at, json.optString(created_at));
		user.put(following, json.optBoolean(following));
		user.put(allow_all_act_msg, json.optBoolean(allow_all_act_msg));
		user.put(geo_enabled, json.optBoolean(geo_enabled));
		user.put(verified, json.optBoolean(verified));
		user.put(verified_type, json.optInt(verified_type));
		user.put(remark, json.optString(remark));
//		user.put(ptype, json.optInt(ptype));
		user.put(allow_all_comment, json.optBoolean(allow_all_comment));
		user.put(avatar_large, json.optString(avatar_large));
		user.put(avatar_hd, json.optString(avatar_hd));
		user.put(verified_reason, json.optString(verified_reason));
		user.put(follow_me, json.optBoolean(follow_me));
		user.put(online_status, json.optInt(online_status));
		user.put(bi_followers_count, json.optInt(bi_followers_count));
		user.put(lang, json.optString(lang));
//		user.put(star, json.optString(star));
//		user.put(mbtype, json.optInt(mbtype));
//		user.put(mbrank, json.optInt(mbrank));
//		user.put(block_word, json.optInt(block_word));
		// 添加最新微博id
		if (json.has(SINGLE)) {
			user.put(status_id, json.optJSONObject(Status.SINGLE).optLong(Constants.ID));
		}
		return user;
	}
} public class WeiboAPIError implements Parcelable {

	private static final String TAG = "WeiboAPIError";

	public static final String ERROR_CODE = "error_code";
	public static final String REQUEST = "request";
	public static final String ERROR = "error";

	public final int error_code;
	public final String request;
	public final String error;

	public WeiboAPIError(int error_code, String request, String error) {
		this.error_code = error_code;
		this.request = request;
		this.error = error;
	}

	/**
	 * 从volley中解析错误
	 *
	 * @param volleyError
	 * @return WeiboAPIError
	 */
	public static WeiboAPIError fromVolleyError(VolleyError volleyError) {
		// very bad situation!
		if (volleyError.networkResponse == null) {
			String message = volleyError.getLocalizedMessage();
			return new WeiboAPIError(-1, null,
					TextUtils.isEmpty(message) ? volleyError.getClass().getName() : message);
		}
		// 很可能出现response body为空的情况
		String jsonString;
		try {
			jsonString = new String(volleyError.networkResponse.data);
		} catch (Exception e) {
			Log.wtf(TAG, e);
			// fall back to http error!
			String msg = volleyError.getLocalizedMessage();
			return new WeiboAPIError(
					volleyError.networkResponse.statusCode,
					null,
					TextUtils.isEmpty(msg) ? "Unknown error! Please try again later:-(" : msg);
		}

		try {
			JSONObject jsonObject = new JSONObject(jsonString);
			return new WeiboAPIError(
					jsonObject.optInt(ERROR_CODE),
					jsonObject.optString(REQUEST),
					jsonObject.optString(ERROR)
			);
		} catch (JSONException e) {
			Log.wtf(TAG, e.getLocalizedMessage());
		}
		// never happen！
		return null;
	}

	@Override
	public int describeContents() {
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int flags) {
		dest.writeInt(error_code);
		dest.writeString(request);
		dest.writeString(error);
	}

	public static final Parcelable.Creator<WeiboAPIError> CREATOR = new Creator<WeiboAPIError>() {
		@Override
		public WeiboAPIError createFromParcel(Parcel source) {
			return new WeiboAPIError(
				source.readInt(),
				source.readString(),
				source.readString()
			);
		}

		@Override
		public WeiboAPIError[] newArray(int size) {
			return new WeiboAPIError[size];
		}
	};
} public class StatusProcessor {

	/**
	 * 判断是不是新浪在时间线中强奸的广告
	 *
	 * @param weibo json
	 * @return if it' s ad. or malformed data true or false
	 */
	public static final boolean fuckSina(JSONObject weibo) {
		if (weibo == null) {
			return true;
		}
		if (weibo.has(User.SINGLE)) {
			// 如果不是我关注的用户，那么基本上就是新浪的广告了，fuck
			JSONObject user = weibo.optJSONObject(User.SINGLE);
			if (user == null || !user.optBoolean(User.following)) {
				return true;
			}
		}
		// todo: 如果返回的微博是没有user这个字段，只有uid咋整呢?
		return false;
	}

	/**
	 * 持久化时间线
	 *
	 * @author longkai
	 */
	public static class TimelineProcessor implements CatnutProcessor<JSONObject> {

		private int type = Status.HOME;

		private boolean hasUser = true;

		public TimelineProcessor(boolean hasUser) {
			this.hasUser = hasUser;
		}

		public TimelineProcessor(int type, boolean hasUser) {
			this.type = type;
			this.hasUser = hasUser;
		}

		public void asyncProcess(Context context, JSONObject jsonObject) throws Exception {
			JSONArray jsonArray = jsonObject.optJSONArray(Status.MULTIPLE);
			ContentValues[] statuses = new ContentValues[jsonArray.length()];
			ContentValues[] users = null;
			if (hasUser) {
				users = new ContentValues[statuses.length];
			}
			User userMetadata = User.METADATA;
			Status statusMetadata = Status.METADATA;
			JSONObject json;
			ContentValues status;
			for (int i = 0; i < jsonArray.length(); i++) {
				json = jsonArray.optJSONObject(i);
				// 过滤一下不要的微博
				if (type == Status.HOME && fuckSina(json)) {
					continue;
				}
				// 一次持久化微博和作者信息
				status = statusMetadata.convert(json);
				status.put(Status.TYPE, type); // 标记为啥类型的微博
				statuses[i] = status;
				// 如果这条微博包含了原作者信息
				if (hasUser && json.has(User.SINGLE)) {
					users[i] = userMetadata.convert(json.optJSONObject(User.SINGLE));
				}
			}
			context.getContentResolver().bulkInsert(CatnutProvider.parse(Status.MULTIPLE), statuses);
			if (hasUser) {
				context.getContentResolver().bulkInsert(CatnutProvider.parse(User.MULTIPLE), users);
			}
		}
	}

	/**
	 * 持久化评论时间线
	 *
	 * @author longkai
	 */
	public static class CommentTweetsProcessor implements CatnutProcessor<JSONObject> {

		/** 改评论指向的微博id */
		private long to;

		public CommentTweetsProcessor(long to) {
			this.to = to;
		}

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			JSONArray array = data.optJSONArray(Status.COMMENTS);
			ContentValues[] comments = new ContentValues[array.length()];
			ContentValues[] users = new ContentValues[array.length()];
			ContentValues comment;
			JSONObject jsonUser;
			JSONObject jsonStatus;
			for (int i = 0; i < comments.length; i++) {
				// 解析微博
				jsonStatus = array.optJSONObject(i);
				comment = Status.METADATA.convert(jsonStatus);
				comment.put(Status.TYPE, Status.COMMENT); // 标记为评论微博
				comment.put(Status.TO_WHICH_TWEET, to);
				comments[i] = comment;
				// 解析评论作者
				jsonUser = jsonStatus.optJSONObject(User.SINGLE);
				users[i] = User.METADATA.convert(jsonUser);
			}
			// 持久化
			context.getContentResolver().bulkInsert(CatnutProvider.parse(Status.MULTIPLE), comments);
			context.getContentResolver().bulkInsert(CatnutProvider.parse(User.MULTIPLE), users);
		}
	}

	/**
	 * 当前登录用户收到的评论
	 *
	 * @author longkai
	 */
	public static class Comments2MeProcessor implements CatnutProcessor<JSONObject> {
		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			JSONArray commentsArray = data.optJSONArray(Status.COMMENTS);
			ContentValues[] comments = new ContentValues[commentsArray.length()];
			ContentValues[] users = new ContentValues[comments.length];
			JSONObject json;
			for (int i = 0; i < comments.length; i++) {
				json = commentsArray.optJSONObject(i);
				comments[i] = Comment.METADATA.convert(json);
				users[i] = User.METADATA.convert(json.optJSONObject(User.SINGLE));
			}
			context.getContentResolver().bulkInsert(CatnutProvider.parse(Comment.MULTIPLE), comments);
			context.getContentResolver().bulkInsert(CatnutProvider.parse(User.MULTIPLE), users);
		}
	}

	/**
	 * 处理单条评论
	 *
	 * @author longkai
	 */
	public static class CommentTweetProcessor implements CatnutProcessor<JSONObject> {

		private long id;

		public CommentTweetProcessor(long id) {
			this.id = id;
		}

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			ContentValues comment = new ContentValues();
			comment = Status.METADATA.convert(data);
			comment.put(Status.TYPE, Status.COMMENT);
			comment.put(Status.TO_WHICH_TWEET, id);

			context.getContentResolver().insert(CatnutProvider.parse(Status.MULTIPLE), comment);
			String increment = CatnutUtils.increment(true, Status.TABLE, Status.comments_count, id);
			context.getContentResolver().update(CatnutProvider.parse(Status.MULTIPLE), null, increment, null);
		}
	}

	/**
	 * 收藏/取消收藏处理器
	 *
	 * @author longkai
	 */
	public static class FavoriteTweetProcessor implements CatnutProcessor<JSONObject> {

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			JSONObject jsonObject = data.optJSONObject(Status.SINGLE);
			StringBuilder update = new StringBuilder("UPDATE ").append(Status.TABLE);
			update.append(" SET ").append(Status.favorited).append("=")
					.append(jsonObject.optBoolean(Status.favorited) ? "1" : "0")
					.append(" WHERE ").append(BaseColumns._ID).append("=").append(jsonObject.getLong(Constants.ID));
			context.getContentResolver().update(CatnutProvider.parse(Status.MULTIPLE), null, update.toString(), null);
			// 更新用户数据
			JSONObject user = jsonObject.optJSONObject(User.SINGLE);
			if (user != null) {
				String increment = CatnutUtils.increment(true, User.TABLE, User.favourites_count, user.getLong(Constants.ID));
				context.getContentResolver().update(CatnutProvider.parse(User.MULTIPLE), null, increment, null);
			}
		}
	}

	/**
	 * 持久化收藏列表
	 *
	 * @author longkai
	 */
	public static class FavoriteTweetsProcessor implements CatnutProcessor<JSONObject> {

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			int delete = 0;
			JSONArray array = data.optJSONArray(Status.FAVORITES);
			ContentValues[] favorites = new ContentValues[array.length()];
			ContentValues[] users = new ContentValues[favorites.length];
			JSONObject json;
			for (int i = 0; i < favorites.length; i++) {
				// 解析微博，这里共用了变量名
				json = array.optJSONObject(i).optJSONObject(Status.SINGLE);
				if (json.optInt("deleted") == 1) {
					// 可能微博已经被删除了
					delete++;
					continue;
				}
				ContentValues values = Status.METADATA.convert(json);
				values.put(Status.favorited, 1);
				favorites[i] = values;
				// 解析用户
				users[i] = User.METADATA.convert(json.optJSONObject(User.SINGLE));
			}
			// persist
			context.getContentResolver().bulkInsert(CatnutProvider.parse(User.MULTIPLE), users);
			context.getContentResolver().bulkInsert(CatnutProvider.parse(Status.MULTIPLE), favorites);
			data.put(FavoriteFragment.TAG, delete);
		}
	}

	/**
	 * 普通的一条微博解析，需要提供一个微博类型
	 *
	 * @author longkai
	 */
	public static class SingleTweetProcessor implements CatnutProcessor<JSONObject> {

		// 微博类型
		private int type;

		public SingleTweetProcessor(int type) {
			this.type = type;
		}

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			ContentValues status = Status.METADATA.convert(data);
			status.put(Status.TYPE, type); // 添加类型
			ContentValues user = User.METADATA.convert(data.optJSONObject(User.SINGLE));
			context.getContentResolver().insert(CatnutProvider.parse(Status.MULTIPLE), status);
			context.getContentResolver().insert(CatnutProvider.parse(User.MULTIPLE), user);
			// 如果是转发微博的话，自增一下
			if (type == Status.RETWEET) {
				long id = data.optJSONObject(Status.retweeted_status).optLong(Constants.ID);
				String increment = CatnutUtils.increment(true, Status.TABLE, Status.reposts_count, id);
				context.getContentResolver().update(CatnutProvider.parse(Status.MULTIPLE), null, increment, null);
			}
		}
	}
} public class PageTransformer {

	public static class DepthPageTransformer implements ViewPager.PageTransformer {

		private static final float MIN_SCALE = 0.75f;

		public void transformPage(View view, float position) {
			int pageWidth = view.getWidth();

			if (position < -1) { // [-Infinity,-1)
				// This page is way off-screen to the left.
				view.setAlpha(0);

			} else if (position <= 0) { // [-1,0]
				// Use the default slide transition when moving to the left page
				view.setAlpha(1);
				view.setTranslationX(0);
				view.setScaleX(1);
				view.setScaleY(1);

			} else if (position <= 1) { // (0,1]
				// Fade the page out.
				view.setAlpha(1 - position);

				// Counteract the default slide transition
				view.setTranslationX(pageWidth * -position);

				// Scale the page down (between MIN_SCALE and 1)
				float scaleFactor = MIN_SCALE
						+ (1 - MIN_SCALE) * (1 - Math.abs(position));
				view.setScaleX(scaleFactor);
				view.setScaleY(scaleFactor);

			} else { // (1,+Infinity]
				// This page is way off-screen to the right.
				view.setAlpha(0);
			}
		}
	}

	public static class ZoomOutPageTransformer implements ViewPager.PageTransformer {
		private static final float MIN_SCALE = 0.85f;
		private static final float MIN_ALPHA = 0.5f;

		public void transformPage(View view, float position) {
			int pageWidth = view.getWidth();
			int pageHeight = view.getHeight();

			if (position < -1) { // [-Infinity,-1)
				// This page is way off-screen to the left.
				view.setAlpha(0);

			} else if (position <= 1) { // [-1,1]
				// Modify the default slide transition to shrink the page as well
				float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
				float vertMargin = pageHeight * (1 - scaleFactor) / 2;
				float horzMargin = pageWidth * (1 - scaleFactor) / 2;
				if (position < 0) {
					view.setTranslationX(horzMargin - vertMargin / 2);
				} else {
					view.setTranslationX(-horzMargin + vertMargin / 2);
				}

				// Scale the page down (between MIN_SCALE and 1)
				view.setScaleX(scaleFactor);
				view.setScaleY(scaleFactor);

				// Fade the page relative to its size.
				view.setAlpha(MIN_ALPHA +
						(scaleFactor - MIN_SCALE) /
								(1 - MIN_SCALE) * (1 - MIN_ALPHA));

			} else { // (1,+Infinity]
				// This page is way off-screen to the right.
				view.setAlpha(0);
			}
		}
	}
} public class TouchImageView extends ImageView {

	private static final String DEBUG = "DEBUG";

	//
	// SuperMin and SuperMax multipliers. Determine how much the image can be
	// zoomed below or above the zoom boundaries, before animating back to the
	// min/max zoom boundary.
	//
	private static final float SUPER_MIN_MULTIPLIER = .75f;
	private static final float SUPER_MAX_MULTIPLIER = 1.25f;

	//
	// Scale of image ranges from minScale to maxScale, where minScale == 1
	// when the image is stretched to fit view.
	//
	private float normalizedScale;

	//
	// Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
	// MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix
	// saved prior to the screen rotating.
	//
	private Matrix matrix, prevMatrix;

	public static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM };
	private State state;

	private float minScale;
	private float maxScale;
	private float superMinScale;
	private float superMaxScale;
	private float[] m;

	private Context context;
	private Fling fling;

	private ScaleType mScaleType;

	//
	// Size of view and previous view size (ie before rotation)
	//
	private int viewWidth, viewHeight, prevViewWidth, prevViewHeight;

	//
	// Size of image when it is stretched to fit view. Before and After rotation.
	//
	private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight;

	private ScaleGestureDetector mScaleDetector;
	private GestureDetector mGestureDetector;

	public TouchImageView(Context context) {
		super(context);
		sharedConstructing(context);
	}

	public TouchImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		sharedConstructing(context);
	}

	public TouchImageView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		sharedConstructing(context);
	}

	private void sharedConstructing(Context context) {
		super.setClickable(true);
		this.context = context;
		mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
		mGestureDetector = new GestureDetector(context, new GestureListener());
		matrix = new Matrix();
		prevMatrix = new Matrix();
		m = new float[9];
		normalizedScale = 1;
		mScaleType = ScaleType.FIT_CENTER;
		minScale = 1;
		maxScale = 3;
		superMinScale = SUPER_MIN_MULTIPLIER * minScale;
		superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
		setImageMatrix(matrix);
		setScaleType(ScaleType.MATRIX);
		setState(NONE);
		setOnTouchListener(new TouchImageViewListener());
	}

	@Override
	public void setImageResource(int resId) {
		super.setImageResource(resId);
		savePreviousImageValues();
		fitImageToView();
	}

	@Override
	public void setImageBitmap(Bitmap bm) {
		super.setImageBitmap(bm);
		savePreviousImageValues();
		fitImageToView();
	}

	@Override
	public void setImageDrawable(Drawable drawable) {
		super.setImageDrawable(drawable);
		savePreviousImageValues();
		fitImageToView();
	}

	@Override
	public void setImageURI(Uri uri) {
		super.setImageURI(uri);
		savePreviousImageValues();
		fitImageToView();
	}

	@Override
	public void setScaleType(ScaleType type) {
		if (type == ScaleType.FIT_START || type == ScaleType.FIT_END) {
			throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");
		}
		if (type == ScaleType.MATRIX) {
			super.setScaleType(ScaleType.MATRIX);

		} else {
			mScaleType = type;
		}
	}

	@Override
	public ScaleType getScaleType() {
		return mScaleType;
	}

	/**
	 * Returns false if image is in initial, unzoomed state. False, otherwise.
	 * @return true if image is zoomed
	 */
	public boolean isZoomed() {
		return normalizedScale != 1;
	}

	/**
	 * Return a bitmap of the zoomed image as it appears within the view. This essentially
	 * acts as a "screenshot" of the view and the size of the final bitmap is limited to the
	 * resolution of the view itself.
	 * @return bitmap of zoomed image
	 */
	public Bitmap getZoomedImage() {
		Bitmap bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(bitmap);
		layout(0, 0, viewWidth, viewHeight);
		draw(canvas);
		return bitmap;
	}

	/**
	 * Return a bitmap of the zoomed image. This method is different from getZoomedImage() because
	 * it cuts the image directly from the drawable source, and thus, is not limited by the resolution
	 * of the view. Not supported with FIT_XY.
	 * @return bitmap of zoomed image
	 */
	public Bitmap getZoomedImageFromSource() {
		if (mScaleType == ScaleType.FIT_XY) {
			throw new UnsupportedOperationException("getZoomedImageFromSource() not supported with FIT_XY");
		}
		Bitmap bitmap = ((BitmapDrawable) getDrawable()).getBitmap();
		Rect r = getZoomedRect();
		if (r.width() <= 0 || r.height() <= 0) {
			return null;
		}
		return Bitmap.createBitmap(bitmap, r.left, r.top, r.width(), r.height());
	}

	/**
	 * Return a Rect representing the zoomed image in the coordinate system of the drawable source.
	 * @return rect representing zoomed image
	 */
	public Rect getZoomedRect() {
		if (mScaleType == ScaleType.FIT_XY) {
			throw new UnsupportedOperationException("getZoomedRect() not supported with FIT_XY");
		}
		PointF topLeft = getDrawablePointFromTouchPoint(0,0);
		PointF bottomRight = getDrawablePointFromTouchPoint(viewWidth, viewHeight);
		return new Rect((int) topLeft.x, (int) topLeft.y,(int) bottomRight.x, (int) bottomRight.y);
	}

	/**
	 * Save the current matrix and view dimensions
	 * in the prevMatrix and prevView variables.
	 */
	private void savePreviousImageValues() {
		if (matrix != null) {
			matrix.getValues(m);
			prevMatrix.setValues(m);
			prevMatchViewHeight = matchViewHeight;
			prevMatchViewWidth = matchViewWidth;
			prevViewHeight = viewHeight;
			prevViewWidth = viewWidth;
		}
	}

	@Override
	public Parcelable onSaveInstanceState() {
		Bundle bundle = new Bundle();
		bundle.putParcelable("instanceState", super.onSaveInstanceState());
		bundle.putFloat("saveScale", normalizedScale);
		bundle.putFloat("matchViewHeight", matchViewHeight);
		bundle.putFloat("matchViewWidth", matchViewWidth);
		bundle.putInt("viewWidth", viewWidth);
		bundle.putInt("viewHeight", viewHeight);
		matrix.getValues(m);
		bundle.putFloatArray("matrix", m);
		return bundle;
	}

	@Override
	public void onRestoreInstanceState(Parcelable state) {
		if (state instanceof Bundle) {
			Bundle bundle = (Bundle) state;
			normalizedScale = bundle.getFloat("saveScale");
			m = bundle.getFloatArray("matrix");
			prevMatrix.setValues(m);
			prevMatchViewHeight = bundle.getFloat("matchViewHeight");
			prevMatchViewWidth = bundle.getFloat("matchViewWidth");
			prevViewHeight = bundle.getInt("viewHeight");
			prevViewWidth = bundle.getInt("viewWidth");
			super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
			return;
		}

		super.onRestoreInstanceState(state);
	}

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		savePreviousImageValues();
	}

	/**
	 * Get the max zoom multiplier.
	 * @return max zoom multiplier.
	 */
	public float getMaxZoom() {
		return maxScale;
	}

	/**
	 * Set the max zoom multiplier. Default value: 3.
	 * @param max max zoom multiplier.
	 */
	public void setMaxZoom(float max) {
		maxScale = max;
		superMaxScale = SUPER_MAX_MULTIPLIER * maxScale;
	}

	/**
	 * Get the min zoom multiplier.
	 * @return min zoom multiplier.
	 */
	public float getMinZoom() {
		return minScale;
	}

	/**
	 * Get the current zoom. This is the zoom relative to the initial
	 * scale, not the original resource.
	 * @return current zoom multiplier.
	 */
	public float getCurrentZoom() {
		return normalizedScale;
	}

	/**
	 * Set the min zoom multiplier. Default value: 1.
	 * @param min min zoom multiplier.
	 */
	public void setMinZoom(float min) {
		minScale = min;
		superMinScale = SUPER_MIN_MULTIPLIER * minScale;
	}

	/**
	 * Reset zoom and translation to initial state.
	 */
	public void resetZoom() {
		normalizedScale = 1;
		fitImageToView();
	}

	/**
	 * Set zoom to the specified scale. Image will be centered by default.
	 * @param scale
	 */
	public void setZoom(float scale) {
		setZoom(scale, 0.5f, 0.5f);
	}

	/**
	 * Set zoom to the specified scale. Image will be centered around the point
	 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
	 * as a fraction from the left and top of the view. For example, the top left
	 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
	 * @param scale
	 * @param focusX
	 * @param focusY
	 */
	public void setZoom(float scale, float focusX, float focusY) {
		setZoom(scale, focusX, focusY, mScaleType);
	}

	/**
	 * Set zoom to the specified scale. Image will be centered around the point
	 * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
	 * as a fraction from the left and top of the view. For example, the top left
	 * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
	 * @param scale
	 * @param focusX
	 * @param focusY
	 * @param scaleType
	 */
	public void setZoom(float scale, float focusX, float focusY, ScaleType scaleType) {
		if (scale < minScale || scale > maxScale) {
			throw new UnsupportedOperationException("Scale must be greater than minScale and less than maxScale");
		}
		if (focusX < 0 || focusX > 1 || focusY < 0 || focusY > 1) {
			throw new UnsupportedOperationException("focusX and focusY must range in value between 0 and 1");
		}
		setScaleType(scaleType);
		resetZoom();
		scaleImage(scale, viewWidth / 2, viewHeight / 2, false);
		matrix.getValues(m);
		m[Matrix.MTRANS_X] = -((focusX * getImageWidth()) - (viewWidth * 0.5f));
		m[Matrix.MTRANS_Y] = -((focusY * getImageHeight()) - (viewHeight * 0.5f));
		matrix.setValues(m);
		setImageMatrix(matrix);
	}

	/**
	 * Set zoom parameters equal to another TouchImageView. Including scale, position,
	 * and ScaleType.
	 * @param img
	 */
	public void setZoom(TouchImageView img) {
		PointF center = img.getCenterOfZoomedImage();
		setZoom(img.getCurrentZoom(), center.x, center.y, img.getScaleType());
	}

	/**
	 * For a given point on the view (ie, a touch event), returns the
	 * point relative to the original drawable's coordinate system.
	 * @param x
	 * @param y
	 * @return PointF relative to original drawable's coordinate system.
	 */
	public PointF getDrawablePointFromTouchPoint(float x, float y) {
		return transformCoordTouchToBitmap(x, y, true);
	}

	/**
	 * For a given point on the view (ie, a touch event), returns the
	 * point relative to the original drawable's coordinate system.
	 * @param p
	 * @return PointF relative to original drawable's coordinate system.
	 */
	public PointF getDrawablePointFromTouchPoint(PointF p) {
		return transformCoordTouchToBitmap(p.x, p.y, true);
	}

	/**
	 * Return the point at the center of the zoomed image. The PointF coordinates range
	 * in value between 0 and 1 and the focus point is denoted as a fraction from the left
	 * and top of the view. For example, the top left corner of the image would be (0, 0).
	 * And the bottom right corner would be (1, 1).
	 * @return PointF representing center of zoomed image
	 */
	public PointF getCenterOfZoomedImage() {
		Drawable drawable = getDrawable();
		if (drawable == null) {
			return null;
		}
		int drawableWidth = drawable.getIntrinsicWidth();
		int drawableHeight = drawable.getIntrinsicHeight();

		PointF point = getDrawablePointFromTouchPoint(viewWidth / 2, viewHeight / 2);
		point.x /= drawableWidth;
		point.y /= drawableHeight;
		return point;
	}

	/**
	 * Performs boundary checking and fixes the image matrix if it
	 * is out of bounds.
	 */
	private void fixTrans() {
		matrix.getValues(m);
		float transX = m[Matrix.MTRANS_X];
		float transY = m[Matrix.MTRANS_Y];

		float fixTransX = getFixTrans(transX, viewWidth, getImageWidth());
		float fixTransY = getFixTrans(transY, viewHeight, getImageHeight());

		if (fixTransX != 0 || fixTransY != 0) {
			matrix.postTranslate(fixTransX, fixTransY);
		}
	}

	/**
	 * When transitioning from zooming from focus to zoom from center (or vice versa)
	 * the image can become unaligned within the view. This is apparent when zooming
	 * quickly. When the content size is less than the view size, the content will often
	 * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
	 * then makes sure the image is centered correctly within the view.
	 */
	private void fixScaleTrans() {
		fixTrans();
		matrix.getValues(m);
		if (getImageWidth() < viewWidth) {
			m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2;
		}

		if (getImageHeight() < viewHeight) {
			m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2;
		}
		matrix.setValues(m);
	}

	private float getFixTrans(float trans, float viewSize, float contentSize) {
		float minTrans, maxTrans;

		if (contentSize <= viewSize) {
			minTrans = 0;
			maxTrans = viewSize - contentSize;

		} else {
			minTrans = viewSize - contentSize;
			maxTrans = 0;
		}

		if (trans < minTrans)
			return -trans + minTrans;
		if (trans > maxTrans)
			return -trans + maxTrans;
		return 0;
	}

	private float getFixDragTrans(float delta, float viewSize, float contentSize) {
		if (contentSize <= viewSize) {
			return 0;
		}
		return delta;
	}

	private float getImageWidth() {
		return matchViewWidth * normalizedScale;
	}

	private float getImageHeight() {
		return matchViewHeight * normalizedScale;
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		Drawable drawable = getDrawable();
		if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
			setMeasuredDimension(0, 0);
			return;
		}

		int drawableWidth = drawable.getIntrinsicWidth();
		int drawableHeight = drawable.getIntrinsicHeight();
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		viewWidth = setViewSize(widthMode, widthSize, drawableWidth);
		viewHeight = setViewSize(heightMode, heightSize, drawableHeight);

		//
		// Set view dimensions
		//
		setMeasuredDimension(viewWidth, viewHeight);

		//
		// Fit content within view
		//
		fitImageToView();
	}

	/**
	 * If the normalizedScale is equal to 1, then the image is made to fit the screen. Otherwise,
	 * it is made to fit the screen according to the dimensions of the previous image matrix. This
	 * allows the image to maintain its zoom after rotation.
	 */
	private void fitImageToView() {
		Drawable drawable = getDrawable();
		if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) {
			return;
		}
		if (matrix == null || prevMatrix == null) {
			return;
		}

		int drawableWidth = drawable.getIntrinsicWidth();
		int drawableHeight = drawable.getIntrinsicHeight();

		//
		// Scale image for view
		//
		float scaleX = (float) viewWidth / drawableWidth;
		float scaleY = (float) viewHeight / drawableHeight;

		switch (mScaleType) {
			case CENTER:
				scaleX = scaleY = 1;
				break;

			case CENTER_CROP:
				scaleX = scaleY = Math.max(scaleX, scaleY);
				break;

			case CENTER_INSIDE:
				scaleX = scaleY = Math.min(1, Math.min(scaleX, scaleY));

			case FIT_CENTER:
				scaleX = scaleY = Math.min(scaleX, scaleY);
				break;

			case FIT_XY:
				break;

			default:
				//
				// FIT_START and FIT_END not supported
				//
				throw new UnsupportedOperationException("TouchImageView does not support FIT_START or FIT_END");

		}

		//
		// Center the image
		//
		float redundantXSpace = viewWidth - (scaleX * drawableWidth);
		float redundantYSpace = viewHeight - (scaleY * drawableHeight);
		matchViewWidth = viewWidth - redundantXSpace;
		matchViewHeight = viewHeight - redundantYSpace;
		if (!isZoomed()) {
			//
			// Stretch and center image to fit view
			//
			matrix.setScale(scaleX, scaleY);
			matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2);
			normalizedScale = 1;

		} else {
			prevMatrix.getValues(m);

			//
			// Rescale Matrix after rotation
			//
			m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale;
			m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale;

			//
			// TransX and TransY from previous matrix
			//
			float transX = m[Matrix.MTRANS_X];
			float transY = m[Matrix.MTRANS_Y];

			//
			// Width
			//
			float prevActualWidth = prevMatchViewWidth * normalizedScale;
			float actualWidth = getImageWidth();
			translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth);

			//
			// Height
			//
			float prevActualHeight = prevMatchViewHeight * normalizedScale;
			float actualHeight = getImageHeight();
			translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight);

			//
			// Set the matrix to the adjusted scale and translate values.
			//
			matrix.setValues(m);
		}
		fixTrans();
		setImageMatrix(matrix);
	}

	/**
	 * Set view dimensions based on layout params
	 *
	 * @param mode
	 * @param size
	 * @param drawableWidth
	 * @return
	 */
	private int setViewSize(int mode, int size, int drawableWidth) {
		int viewSize;
		switch (mode) {
			case MeasureSpec.EXACTLY:
				viewSize = size;
				break;

			case MeasureSpec.AT_MOST:
				viewSize = Math.min(drawableWidth, size);
				break;

			case MeasureSpec.UNSPECIFIED:
				viewSize = drawableWidth;
				break;

			default:
				viewSize = size;
				break;
		}
		return viewSize;
	}

	/**
	 * After rotating, the matrix needs to be translated. This function finds the area of image
	 * which was previously centered and adjusts translations so that is again the center, post-rotation.
	 *
	 * @param axis Matrix.MTRANS_X or Matrix.MTRANS_Y
	 * @param trans the value of trans in that axis before the rotation
	 * @param prevImageSize the width/height of the image before the rotation
	 * @param imageSize width/height of the image after rotation
	 * @param prevViewSize width/height of view before rotation
	 * @param viewSize width/height of view after rotation
	 * @param drawableSize width/height of drawable
	 */
	private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) {
		if (imageSize < viewSize) {
			//
			// The width/height of image is less than the view's width/height. Center it.
			//
			m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f;

		} else if (trans > 0) {
			//
			// The image is larger than the view, but was not before rotation. Center it.
			//
			m[axis] = -((imageSize - viewSize) * 0.5f);

		} else {
			//
			// Find the area of the image which was previously centered in the view. Determine its distance
			// from the left/top side of the view as a fraction of the entire image's width/height. Use that percentage
			// to calculate the trans in the new view width/height.
			//
			float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize;
			m[axis] = -((percentage * imageSize) - (viewSize * 0.5f));
		}
	}

	private void setState(State state) {
		this.state = state;
	}

	@Override
	public boolean canScrollHorizontally(int direction) {
		matrix.getValues(m);
		float x = m[Matrix.MTRANS_X];

		if (getImageWidth() < viewWidth) {
			return false;

		} else if (x >= -1 && direction < 0) {
			return false;

		} else if (Math.abs(x) + viewWidth + 1 >= getImageWidth() && direction > 0) {
			return false;
		}

		return true;
	}

	/**
	 * Gesture Listener detects a single click or long click and passes that on
	 * to the view's listener.
	 * @author Ortiz
	 *
	 */
	private class GestureListener extends GestureDetector.SimpleOnGestureListener {

		@Override
		public boolean onSingleTapConfirmed(MotionEvent e)
		{
			return performClick();
		}

		@Override
		public void onLongPress(MotionEvent e)
		{
			performLongClick();
		}

		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
		{
			if (fling != null) {
				//
				// If a previous fling is still active, it should be cancelled so that two flings
				// are not run simultaenously.
				//
				fling.cancelFling();
			}
			fling = new Fling((int) velocityX, (int) velocityY);
			compatPostOnAnimation(fling);
			return super.onFling(e1, e2, velocityX, velocityY);
		}

		@Override
		public boolean onDoubleTap(MotionEvent e) {
			boolean consumed = false;
			if (state == NONE) {
				float targetZoom = (normalizedScale == minScale) ? maxScale : minScale;
				DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false);
				compatPostOnAnimation(doubleTap);
				consumed = true;
			}
			return consumed;
		}
	}

	/**
	 * Responsible for all touch events. Handles the heavy lifting of drag and also sends
	 * touch events to Scale Detector and Gesture Detector.
	 * @author Ortiz
	 *
	 */
	private class TouchImageViewListener implements OnTouchListener {

		//
		// Remember last point position for dragging
		//
		private PointF last = new PointF();

		@Override
		public boolean onTouch(View v, MotionEvent event) {
			mScaleDetector.onTouchEvent(event);
			mGestureDetector.onTouchEvent(event);
			PointF curr = new PointF(event.getX(), event.getY());

			if (state == NONE || state == DRAG || state == FLING) {
				switch (event.getAction()) {
					case MotionEvent.ACTION_DOWN:
						last.set(curr);
						if (fling != null)
							fling.cancelFling();
						setState(DRAG);
						break;

					case MotionEvent.ACTION_MOVE:
						if (state == DRAG) {
							float deltaX = curr.x - last.x;
							float deltaY = curr.y - last.y;
							float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth());
							float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight());
							matrix.postTranslate(fixTransX, fixTransY);
							fixTrans();
							last.set(curr.x, curr.y);
						}
						break;

					case MotionEvent.ACTION_UP:
					case MotionEvent.ACTION_POINTER_UP:
						setState(NONE);
						break;
				}
			}

			setImageMatrix(matrix);
			//
			// indicate event was handled
			//
			return true;
		}
	}

	/**
	 * ScaleListener detects user two finger scaling and scales image.
	 * @author Ortiz
	 *
	 */
	private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
		@Override
		public boolean onScaleBegin(ScaleGestureDetector detector) {
			setState(ZOOM);
			return true;
		}

		@Override
		public boolean onScale(ScaleGestureDetector detector) {
			scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true);
			return true;
		}

		@Override
		public void onScaleEnd(ScaleGestureDetector detector) {
			super.onScaleEnd(detector);
			setState(NONE);
			boolean animateToZoomBoundary = false;
			float targetZoom = normalizedScale;
			if (normalizedScale > maxScale) {
				targetZoom = maxScale;
				animateToZoomBoundary = true;

			} else if (normalizedScale < minScale) {
				targetZoom = minScale;
				animateToZoomBoundary = true;
			}

			if (animateToZoomBoundary) {
				DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true);
				compatPostOnAnimation(doubleTap);
			}
		}
	}

	private void scaleImage(float deltaScale, float focusX, float focusY, boolean stretchImageToSuper) {

		float lowerScale, upperScale;
		if (stretchImageToSuper) {
			lowerScale = superMinScale;
			upperScale = superMaxScale;

		} else {
			lowerScale = minScale;
			upperScale = maxScale;
		}

		float origScale = normalizedScale;
		normalizedScale *= deltaScale;
		if (normalizedScale > upperScale) {
			normalizedScale = upperScale;
			deltaScale = upperScale / origScale;
		} else if (normalizedScale < lowerScale) {
			normalizedScale = lowerScale;
			deltaScale = lowerScale / origScale;
		}

		matrix.postScale(deltaScale, deltaScale, focusX, focusY);
		fixScaleTrans();
	}

	/**
	 * DoubleTapZoom calls a series of runnables which apply
	 * an animated zoom in/out graphic to the image.
	 * @author Ortiz
	 *
	 */
	private class DoubleTapZoom implements Runnable {

		private long startTime;
		private static final float ZOOM_TIME = 500;
		private float startZoom, targetZoom;
		private float bitmapX, bitmapY;
		private boolean stretchImageToSuper;
		private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
		private PointF startTouch;
		private PointF endTouch;

		DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) {
			setState(ANIMATE_ZOOM);
			startTime = System.currentTimeMillis();
			this.startZoom = normalizedScale;
			this.targetZoom = targetZoom;
			this.stretchImageToSuper = stretchImageToSuper;
			PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
			this.bitmapX = bitmapPoint.x;
			this.bitmapY = bitmapPoint.y;

			//
			// Used for translating image during scaling
			//
			startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
			endTouch = new PointF(viewWidth / 2, viewHeight / 2);
		}

		@Override
		public void run() {
			float t = interpolate();
			float deltaScale = calculateDeltaScale(t);
			scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper);
			translateImageToCenterTouchPosition(t);
			fixScaleTrans();
			setImageMatrix(matrix);

			if (t < 1f) {
				//
				// We haven't finished zooming
				//
				compatPostOnAnimation(this);

			} else {
				//
				// Finished zooming
				//
				setState(NONE);
			}
		}

		/**
		 * Interpolate between where the image should start and end in order to translate
		 * the image so that the point that is touched is what ends up centered at the end
		 * of the zoom.
		 * @param t
		 */
		private void translateImageToCenterTouchPosition(float t) {
			float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
			float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
			PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
			matrix.postTranslate(targetX - curr.x, targetY - curr.y);
		}

		/**
		 * Use interpolator to get t
		 * @return
		 */
		private float interpolate() {
			long currTime = System.currentTimeMillis();
			float elapsed = (currTime - startTime) / ZOOM_TIME;
			elapsed = Math.min(1f, elapsed);
			return interpolator.getInterpolation(elapsed);
		}

		/**
		 * Interpolate the current targeted zoom and get the delta
		 * from the current zoom.
		 * @param t
		 * @return
		 */
		private float calculateDeltaScale(float t) {
			float zoom = startZoom + t * (targetZoom - startZoom);
			return zoom / normalizedScale;
		}
	}

	/**
	 * This function will transform the coordinates in the touch event to the coordinate
	 * system of the drawable that the imageview contain
	 * @param x x-coordinate of touch event
	 * @param y y-coordinate of touch event
	 * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
	 * 			to the bounds of the bitmap size.
	 * @return Coordinates of the point touched, in the coordinate system of the original drawable.
	 */
	private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
		matrix.getValues(m);
		float origW = getDrawable().getIntrinsicWidth();
		float origH = getDrawable().getIntrinsicHeight();
		float transX = m[Matrix.MTRANS_X];
		float transY = m[Matrix.MTRANS_Y];
		float finalX = ((x - transX) * origW) / getImageWidth();
		float finalY = ((y - transY) * origH) / getImageHeight();

		if (clipToBitmap) {
			finalX = Math.min(Math.max(finalX, 0), origW);
			finalY = Math.min(Math.max(finalY, 0), origH);
		}

		return new PointF(finalX , finalY);
	}

	/**
	 * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
	 * drawable's coordinate system to the view's coordinate system.
	 * @param bx x-coordinate in original bitmap coordinate system
	 * @param by y-coordinate in original bitmap coordinate system
	 * @return Coordinates of the point in the view's coordinate system.
	 */
	private PointF transformCoordBitmapToTouch(float bx, float by) {
		matrix.getValues(m);
		float origW = getDrawable().getIntrinsicWidth();
		float origH = getDrawable().getIntrinsicHeight();
		float px = bx / origW;
		float py = by / origH;
		float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px;
		float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py;
		return new PointF(finalX , finalY);
	}

	/**
	 * Fling launches sequential runnables which apply
	 * the fling graphic to the image. The values for the translation
	 * are interpolated by the Scroller.
	 * @author Ortiz
	 *
	 */
	private class Fling implements Runnable {

		CompatScroller scroller;
		int currX, currY;

		Fling(int velocityX, int velocityY) {
			setState(FLING);
			scroller = new CompatScroller(context);
			matrix.getValues(m);

			int startX = (int) m[Matrix.MTRANS_X];
			int startY = (int) m[Matrix.MTRANS_Y];
			int minX, maxX, minY, maxY;

			if (getImageWidth() > viewWidth) {
				minX = viewWidth - (int) getImageWidth();
				maxX = 0;

			} else {
				minX = maxX = startX;
			}

			if (getImageHeight() > viewHeight) {
				minY = viewHeight - (int) getImageHeight();
				maxY = 0;

			} else {
				minY = maxY = startY;
			}

			scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX,
					maxX, minY, maxY);
			currX = startX;
			currY = startY;
		}

		public void cancelFling() {
			if (scroller != null) {
				setState(NONE);
				scroller.forceFinished(true);
			}
		}

		@Override
		public void run() {
			if (scroller.isFinished()) {
				scroller = null;
				return;
			}

			if (scroller.computeScrollOffset()) {
				int newX = scroller.getCurrX();
				int newY = scroller.getCurrY();
				int transX = newX - currX;
				int transY = newY - currY;
				currX = newX;
				currY = newY;
				matrix.postTranslate(transX, transY);
				fixTrans();
				setImageMatrix(matrix);
				compatPostOnAnimation(this);
			}
		}
	}

	@TargetApi(Build.VERSION_CODES.GINGERBREAD)
	private class CompatScroller {
		Scroller scroller;
		OverScroller overScroller;
		boolean isPreGingerbread;

		public CompatScroller(Context context) {
			if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
				isPreGingerbread = true;
				scroller = new Scroller(context);

			} else {
				isPreGingerbread = false;
				overScroller = new OverScroller(context);
			}
		}

		public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
			if (isPreGingerbread) {
				scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
			} else {
				overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
			}
		}

		public void forceFinished(boolean finished) {
			if (isPreGingerbread) {
				scroller.forceFinished(finished);
			} else {
				overScroller.forceFinished(finished);
			}
		}

		public boolean isFinished() {
			if (isPreGingerbread) {
				return scroller.isFinished();
			} else {
				return overScroller.isFinished();
			}
		}

		public boolean computeScrollOffset() {
			if (isPreGingerbread) {
				return scroller.computeScrollOffset();
			} else {
				return overScroller.computeScrollOffset();
			}
		}

		public int getCurrX() {
			if (isPreGingerbread) {
				return scroller.getCurrX();
			} else {
				return overScroller.getCurrX();
			}
		}

		public int getCurrY() {
			if (isPreGingerbread) {
				return scroller.getCurrY();
			} else {
				return overScroller.getCurrY();
			}
		}
	}

	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
	private void compatPostOnAnimation(Runnable runnable) {
		if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
			postOnAnimation(runnable);

		} else {
			postDelayed(runnable, 1000/60);
		}
	}

	private void printMatrixInfo() {
		matrix.getValues(m);
		Log.d(DEBUG, "Scale: " + m[Matrix.MSCALE_X] + " TransX: " + m[Matrix.MTRANS_X] + " TransY: " + m[Matrix.MTRANS_Y]);
	}
} public class TweetImageSpan implements Html.ImageGetter {

	private static final String TAG = "TweetImageSpan";
	/** 默认表情存放目录 */
	public static final String EMOTIONS_DIR = "emotions/";
	/** 微博表情匹配 [xx] */
	public static final Pattern EMOTION_PATTEN = Pattern.compile("\\[([^\\]\\[/ ]+)\\]");

	private Context mContext;

	// 为了保持有序，so换成linked的啦
	public static final Map<String, String> EMOTIONS = new LinkedHashMap<String, String>();

	static {
		EMOTIONS.put("[拜拜]", "baibai.png");
		EMOTIONS.put("[奥特曼]", "aoteman.png");
		EMOTIONS.put("[鄙视]", "bishi.png");
		EMOTIONS.put("[失望]", "shiwang.png");
		EMOTIONS.put("[闭嘴]", "bizui.png");
		EMOTIONS.put("[吃惊]", "chijing.png");
		EMOTIONS.put("[来]", "come_on.png");
		EMOTIONS.put("[酷]", "cool.png");
		EMOTIONS.put("[抓狂]", "zhuakuang.png");
		EMOTIONS.put("[衰]", "shuai.png");
		EMOTIONS.put("[馋嘴]", "chanzui.png");
		EMOTIONS.put("[晕]", "yun.png");
		EMOTIONS.put("[给力]", "geili.png");
		EMOTIONS.put("[浮云]", "fuyun.png");
		EMOTIONS.put("[good]", "good.png");
		EMOTIONS.put("[鼓掌]", "guzhang.png");
		EMOTIONS.put("[黑线]", "heixian.png");
		EMOTIONS.put("[哼]", "heng.png");
		EMOTIONS.put("[心]", "xin.png");
		EMOTIONS.put("[偷笑]", "touxiao.png");
		EMOTIONS.put("[神马]", "shenma.png");
		EMOTIONS.put("[花心]", "huaxin.png");
		EMOTIONS.put("[互粉]", "hufen.png");
		EMOTIONS.put("[囧]", "jiong.png");
		EMOTIONS.put("[打哈欠]", "kun.png");
		EMOTIONS.put("[挖鼻屎]", "wabishi.png");
		EMOTIONS.put("[可怜]", "kelian.png");
		EMOTIONS.put("[哈哈]", "haha.png");
		EMOTIONS.put("[蜡烛]", "lazhu.png");
		EMOTIONS.put("[懒得理你]", "landelini.png");
		EMOTIONS.put("[礼物]", "liwu.png");
		EMOTIONS.put("[爱你]", "aini.png");
		EMOTIONS.put("[马上有对象]", "duixiang.png");
		EMOTIONS.put("[太开心]", "taikaixin.png");
		EMOTIONS.put("[钱]", "money.png");
		EMOTIONS.put("[怒骂]", "numa.png");
		EMOTIONS.put("[不要]", "buyao.png");
		EMOTIONS.put("[ok]", "ok.png");
		EMOTIONS.put("[熊猫]", "xiongmao.png");
		EMOTIONS.put("[猪头]", "zhutou.png");
		EMOTIONS.put("[亲亲]", "qinqin.png");
		EMOTIONS.put("[兔子]", "tuzi.png");
		EMOTIONS.put("[弱]", "ruo.png");
		EMOTIONS.put("[泪]", "lei.png");
		EMOTIONS.put("[生病]", "shengbing.png");
		EMOTIONS.put("[害羞]", "haixiu.png");
		EMOTIONS.put("[思考]", "sikao.png");
		EMOTIONS.put("[睡觉]", "shuijiao.png");
		EMOTIONS.put("[呵呵]", "hehe.png");
		EMOTIONS.put("[汗]", "han.png");
		EMOTIONS.put("[吐]", "tu.png");
		EMOTIONS.put("[嘻嘻]", "xixi.png");
		EMOTIONS.put("[可爱]", "keai.png");
		EMOTIONS.put("[雪]", "xue.png");
		EMOTIONS.put("[伤心]", "shangxin.png");
		EMOTIONS.put("[威武]", "v5.png");
		EMOTIONS.put("[委屈]", "weiqu.png");
		EMOTIONS.put("[嘘]", "xu.png");
		EMOTIONS.put("[耶]", "ye.png");
		EMOTIONS.put("[左哼哼]", "zuohengheng.png");
		EMOTIONS.put("[右哼哼]", "youhengheng.png");
		EMOTIONS.put("[疑问]", "yiwen.png");
		EMOTIONS.put("[阴险]", "yinxian.png");
		EMOTIONS.put("[赞]", "zan.png");
		EMOTIONS.put("[挤眼]", "zuoguilian.png");
		EMOTIONS.put("[做鬼脸]", "zuoguilian.png"); // 可能有重复，我也搞不明白新浪怎么设置表情的...
		EMOTIONS.put("[玩去啦]", "lvxing.png");
		EMOTIONS.put("[悲伤]", "beishang.png");
		EMOTIONS.put("[怒]", "nu.png");
		EMOTIONS.put("[马到成功]", "madaochenggong.png");
		EMOTIONS.put("[书呆子]", "shudaizi.png");
		EMOTIONS.put("[话筒]", "huatong.png");
		EMOTIONS.put("[蛋糕]", "dangao.png");
		EMOTIONS.put("[感冒]", "ganmao.png");
		EMOTIONS.put("[男孩儿]", "nanhaier.png");
		EMOTIONS.put("[女孩儿]", "nvhaier.png");
		EMOTIONS.put("[顶]", "ding.png");
	}

	// is it worth?
	public static String[] EMOTION_KEYS = EMOTIONS.keySet().toArray(new String[EMOTIONS.size()]);

	private int mIconBound;

	public TweetImageSpan(Context context) {
		this.mContext = context;
		this.mIconBound = context.getResources().getInteger(R.integer.icon_bound_px);
	}

	public Spanned getImageSpan(CharSequence text) {
		Matcher m = EMOTION_PATTEN.matcher(text);
		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			String key = m.group();
			if (EMOTIONS.containsKey(key)) {
				m.appendReplacement(sb, "<img src='" + EMOTIONS.get(key) + "' />");
			}
		}
		m.appendTail(sb);
		return Html.fromHtml(sb.toString(), this, null);
	}

	@Override
	public Drawable getDrawable(String source) {
		Drawable drawable = null;
		InputStream in = null;
		try {
			in = mContext.getAssets().open(EMOTIONS_DIR + source);
			drawable = Drawable.createFromStream(in, null);
		} catch (IOException e) {
			Log.wtf(TAG, "error open assets: " + source, e);
		} finally {
			CatnutUtils.closeIO(in);
		}
		if (drawable != null) {
			if (mIconBound == 0) {
				drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
			} else {
				drawable.setBounds(0, 0, mIconBound, mIconBound);
			}
		}
		return drawable;
	}
} public class TweetTextView extends TextView {

	public static final String TOPIC_SCHEME = "http://huati.weibo.com/k/";
	public static final String MENTION_SCHEME = "org.catnut://profiles/";

	/** 微博中 @xx 正则 */
	public static final Pattern MENTION_PATTERN = Pattern.compile("@[\\u4e00-\\u9fa5a-zA-Z0-9_-]+");
	/** 微博中 #xx# 正则 */
	public static final Pattern TOPIC_PATTERN = Pattern.compile("#[^#]+#");
	/** 微博链接，不包含中文 */
	public static final Pattern WEB_URL = Pattern.compile("(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");

	public static final Linkify.TransformFilter MENTION_FILTER = new Linkify.TransformFilter() {
		@Override
		public String transformUrl(Matcher match, String url) {
			return url.substring(1);
		}
	};

	public static final Linkify.TransformFilter TOPIC_FILTER = new Linkify.TransformFilter() {
		@Override
		public String transformUrl(Matcher match, String url) {
			return url.substring(1, url.length() - 1);
		}
	};

	public static final Linkify.TransformFilter URL_FILTER = new Linkify.TransformFilter() {
		@Override
		public String transformUrl(Matcher match, String url) {
			return url;
		}
	};

	public TweetTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Object text = this.getText();
		if (text instanceof Spanned) {
			Spannable buffer = Spannable.Factory.getInstance().newSpannable((CharSequence) text);

			int action = event.getAction();

			if (action == MotionEvent.ACTION_UP
				|| action == MotionEvent.ACTION_DOWN) {
				int x = (int) event.getX();
				int y = (int) event.getY();

				x -= this.getTotalPaddingLeft();
				y -= this.getTotalPaddingTop();

				x += this.getScrollX();
				y += this.getScrollY();

				Layout layout = this.getLayout();
				int line = layout.getLineForVertical(y);
				int off = layout.getOffsetForHorizontal(line, x);

				ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

				if (link.length != 0) {
					if (action == MotionEvent.ACTION_UP) {
						link[0].onClick(this);
					} else if (action == MotionEvent.ACTION_DOWN) {
						Selection.setSelection(
							buffer,
							buffer.getSpanStart(link[0]),
							buffer.getSpanEnd(link[0])
						);
					}
					return true;
				}
			}
		}
		return false;
	}
} public class ProfileActivity extends Activity implements FragmentManager.OnBackStackChangedListener {

	private CatnutApp mApp;
	private EasyTracker mTracker;

	private Handler mHandler = new Handler();

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mApp = CatnutApp.getTingtingApp();
		if (savedInstanceState == null) {
			final FragmentManager fragmentManager = getFragmentManager();
			fragmentManager.addOnBackStackChangedListener(this);
			Intent intent = getIntent();
			final long uid = intent.getLongExtra(Constants.ID, 0L);
			Uri uri = intent.getData();
			String screenName =
					uri == null ? intent.getStringExtra(User.screen_name) : uri.getLastPathSegment();
			fragmentManager
					.beginTransaction()
					.replace(android.R.id.content, ProfileFragment.getFragment(uid, screenName))
					.commit();
			if (mApp.getPreferences().getBoolean(getString(R.string.pref_enable_analytics), true)) {
				mTracker = EasyTracker.getInstance(this);
			}
		}
		ActionBar bar = getActionBar();
		bar.setIcon(R.drawable.ic_title_profile_default);
		bar.setDisplayHomeAsUpEnabled(true);
	}

	@Override
	protected void onStart() {
		super.onStart();
		if (mTracker != null) {
			mTracker.activityStart(this);
		}
	}

	@Override
	protected void onStop() {
		super.onStop();
		if (mTracker != null) {
			mTracker.activityStop(this);
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		menu.add(Menu.NONE, R.id.pref, Menu.NONE, R.string.pref)
				.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
			case android.R.id.home:
				this.navigateUpTo(getIntent());
				break;
			case R.id.pref:
				startActivity(SingleFragmentActivity.getIntent(this, SingleFragmentActivity.PREF));
				break;
			default:
				break;
		}
		return super.onOptionsItemSelected(item);
	}

	@Override
	public void onBackStackChanged() {
		invalidateOptionsMenu();
	}

	public void flipCard(Fragment fragment, String tag, boolean backStack) {
		FragmentTransaction fragmentTransaction = getFragmentManager()
				.beginTransaction();
		fragmentTransaction
				.setCustomAnimations(
						R.animator.card_flip_right_in, R.animator.card_flip_right_out,
						R.animator.card_flip_left_in, R.animator.card_flip_left_out)
				.replace(android.R.id.content, fragment, tag);
		if (backStack) {
			fragmentTransaction.addToBackStack(null);
		}
		fragmentTransaction.commit();
		mHandler.post(new Runnable() {
			@Override
			public void run() {
				invalidateOptionsMenu();
			}
		});
	}
} public class SingleFragmentActivity extends Activity {

	public static final String TAG = "SingleFragmentActivity";
	public static final int PREF = 0;
	public static final int PHOTO_VIEWER = 1;
	public static final int DRAFT = 2;
	public static final int USER_TWEETS = 3;
	public static final int FRIENDS = 4; // 可以为关注或者粉丝
	public static final int FAVORITES = 5;
	public static final int AUTH = 6;
	public static final int GALLERY = 7;
	public static final int PLUGINS_PREF = 8;

	private EasyTracker mTracker;

	private int mWhich = Integer.MIN_VALUE;
	private ConfirmBarController.Callbacks mCallbacks;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (savedInstanceState == null) {
			FragmentTransaction ft = getFragmentManager().beginTransaction();
			mWhich = getIntent().getIntExtra(TAG, PREF);
			Fragment fragment = null;
			switch (mWhich) {
				case PREF:
					fragment = PrefFragment.getFragment();
					break;
				case PHOTO_VIEWER:
					setTheme(R.style.Theme_Fantasy);
					String picUrl = getIntent().getStringExtra(Constants.PIC);
					fragment = PhotoViewerFragment.getFragment(picUrl);
					break;
				case DRAFT:
					fragment = DraftFragment.getFragment();
					break;
				case FAVORITES:
					fragment = FavoriteFragment.getFragment();
					break;
				case FRIENDS:
					fragment = MyRelationshipFragment.getFragment(getIntent().getBooleanExtra(User.following, true));
					break;
				case USER_TWEETS:
					long id = getIntent().getLongExtra(Constants.ID, 0L);
					String screenName = getIntent().getStringExtra(User.screen_name);
					fragment = UserTimelineFragment.getFragment(id, screenName);
					break;
				case AUTH:
					fragment = new OAuthFragment();
					break;
				case GALLERY:
					setTheme(R.style.Theme_Fantasy);
					int index = getIntent().getIntExtra(GalleryPagerFragment.CUR_INDEX, 0);
					ArrayList<Uri> uris = getIntent().getParcelableArrayListExtra(GalleryPagerFragment.URLS);
					String title = getIntent().getStringExtra(GalleryPagerFragment.TITLE);
					fragment = GalleryPagerFragment.getFragment(index, uris, title);
					break;
				case PLUGINS_PREF:
					fragment = PluginsPrefFragment.getFragment();
					break;
				default:
					// get out!
					navigateUpTo(getIntent());
					break;
			}
			if (fragment instanceof TimelineFragment) {
				mCallbacks = (ConfirmBarController.Callbacks) fragment;
			}
			ft.replace(android.R.id.content, fragment).commit();
		}
		if (CatnutApp.getTingtingApp().getPreferences()
				.getBoolean(getString(R.string.pref_enable_analytics), true)) {
			mTracker = EasyTracker.getInstance(this);
		}
		getActionBar().setDisplayHomeAsUpEnabled(true);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		if (mWhich != PREF && mWhich != PLUGINS_PREF && mWhich != AUTH) {
			menu.add(Menu.NONE, R.id.pref, Menu.NONE, R.string.pref)
					.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
		}
		return true;
	}

	@Override
	public boolean onMenuItemSelected(int featureId, MenuItem item) {
		switch (item.getItemId()) {
			case android.R.id.home:
				if (mWhich == PREF || mWhich == PLUGINS_PREF) {
					onBackPressed();
				} else {
					navigateUpTo(getIntent());
				}
				break;
			case R.id.pref:
				startActivity(getIntent(this, PREF));
				break;
			default:
				break;
		}
		return super.onMenuItemSelected(featureId, item);
	}

	/**
	 * helper method for obtaining a intent which will go to this activity
	 */
	public static Intent getIntent(Context context, int which) {
		Intent intent = new Intent(context, SingleFragmentActivity.class);
		intent.putExtra(TAG, which);
		return intent;
	}

	@Override
	protected void onStart() {
		super.onStart();
		if (mTracker != null) {
			mTracker.activityStart(this);
		}
	}

	@Override
	protected void onStop() {
		super.onStop();
		if (mTracker != null) {
			mTracker.activityStop(this);
		}
	}

	@Override
	protected void onSaveInstanceState(Bundle outState) {
		if (mCallbacks != null) {
			mCallbacks.onActivitySaveInstanceState(outState);
		}
		super.onSaveInstanceState(outState);
	}

	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		super.onRestoreInstanceState(savedInstanceState);
		if (mCallbacks != null) {
			mCallbacks.onActivityRestoreInstanceState(savedInstanceState);
		}
	}
} public class TweetActivity extends Activity {

	private EasyTracker mTracker;

	private OnFragmentBackPressedListener mKeyDownListener;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (CatnutApp.getTingtingApp().getPreferences()
				.getBoolean(getString(R.string.pref_enable_analytics), true)) {
			mTracker = EasyTracker.getInstance(this);
		}
		ActionBar bar = getActionBar();
		bar.setIcon(R.drawable.ic_title_view_tweet);
		bar.setDisplayHomeAsUpEnabled(true);

		long id = getIntent().getLongExtra(Constants.ID, 0L);
		String json = getIntent().getStringExtra(Constants.JSON);
		if (savedInstanceState == null) {
			TweetFragment fragment = id != 0L
					? TweetFragment.getFragment(id)
					: TweetFragment.getFragment(json);
			// 添加back回调
			mKeyDownListener = fragment;
			getFragmentManager().beginTransaction()
					.replace(android.R.id.content, fragment)
					.commit();
		}
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		switch (keyCode) {
			case KeyEvent.KEYCODE_BACK:
				if (mKeyDownListener != null) {
					mKeyDownListener.onBackPressed();
				}
				break;
			default:
				break;
		}
		return super.onKeyDown(keyCode, event);
	}

	@Override
	protected void onStart() {
		super.onStart();
		if (mTracker != null) {
			mTracker.activityStart(this);
		}
	}

	@Override
	protected void onStop() {
		super.onStop();
		if (mTracker != null) {
			mTracker.activityStop(this);
		}
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
			case android.R.id.home:
				if (mKeyDownListener != null) {
					mKeyDownListener.onBackPressed();
				} else {
					this.navigateUpTo(getIntent());
				}
				break;
			default:
				break;
		}
		return super.onOptionsItemSelected(item);
	}
} public class CatnutUtils {

	private static final String TAG = "CatnutUtils";

	/**
	 * cursor默认没有得到boolean类型的值，所以才有了这个方法
	 *
	 * @param cursor
	 * @param columnName
	 * @return x == 1 ? true : false
	 */
	public static boolean getBoolean(Cursor cursor, String columnName) {
		return cursor.getInt(cursor.getColumnIndex(columnName)) == 1 ? true : false;
	}

	/**
	 * 判断实际的int是否为0，否则返回默认的int
	 *
	 * @param real
	 * @param defaultValue
	 * @return int
	 */
	public static int optValue(int real, int defaultValue) {
		return real == 0 ? defaultValue : real;
	}

	/**
	 * 判断实际的long是否为0，否则返回默认的long
	 *
	 * @param real
	 * @param defaultValue
	 * @return long
	 */
	public static long optValue(long real, long defaultValue) {
		return real == 0L ? defaultValue : real;
	}

	/**
	 * 判断实际的String是否为null或者空串，否则返回默认的字符串
	 *
	 * @param real
	 * @param defaultValue
	 * @return String
	 */
	public static String optValue(String real, String defaultValue) {
		return TextUtils.isEmpty(real) ? defaultValue : real;
	}

	/**
	 * 判断实际的float是否为0.0f，否则返回默认的float
	 *
	 * @param real
	 * @param defaultValue
	 * @return float
	 */
	public static float optValue(float real, float defaultValue) {
		return real == 0.0F ? defaultValue : real;
	}

	/**
	 * 判断实际的double是否为0.0D，否则返回默认的double
	 *
	 * @param real
	 * @param defaultValue
	 * @return double
	 */
	public static double optValue(double real, double defaultValue) {
		return real == 0.0D ? defaultValue : real;
	}

	/**
	 * 判断实际的boolean是否为false，否则返回默认的boolean
	 *
	 * @param real
	 * @param defaultValue
	 * @return boolean
	 */
	public static boolean optValue(boolean real, boolean defaultValue) {
		return !real ? defaultValue : real;
	}

	/**
	 * 将数字转换为相对数字，节省版面，比如1234->1.2k，34567->3.5w
	 * <p/>
	 * 注意，0返回null，最大的权值为w
	 *
	 * @param number
	 * @return 数字相对大小字符串
	 */
	public static String approximate(int number) {
		if (number == 0) {
			return null;
		}
		if (number < 1000) {
			return String.valueOf(number);
		} else if (number < 10000) {
			float f = number * 1f / 1000;
			return String.valueOf(Math.round(f * 10) / 10.0) + "k";
		} else {
			float f = number * 1f / 10000;
			return String.valueOf(Math.round(f * 10) / 10.0) + "w";
		}
	}

	/**
	 * 四舍五入float
	 *
	 * @param num 浮点数
	 * @param bit 多少位
	 * @return the  you required
	 */
	public static float scaleNumber(float num, int bit) {
		int range = 10 * bit;
		return (float) Math.round(num * range) / range;
	}

	/**
	 * 设置某个view里面的textview的text
	 *
	 * @param parent     父容器
	 * @param textViewId textview的id
	 * @param text
	 * @return that textview
	 */
	public static TextView setText(View parent, int textViewId, CharSequence text) {
		TextView textView = (TextView) parent.findViewById(textViewId);
		textView.setText(text);
		return textView;
	}

	/**
	 * 自定义raw sql以获取cursor，包含所有的除了having，group外的所有字段
	 *
	 * @param context
	 * @param uri
	 * @param projection
	 * @param selection
	 * @param selectionArgs
	 * @param from
	 * @param joinOn
	 * @param sort
	 * @param limit
	 * @return {@link android.content.CursorLoader}
	 */
	public static CursorLoader getCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String from, String joinOn, String sort, String limit) {
		String sql = buildQuery(projection, selection, from, joinOn, sort, limit);
		return new CursorLoader(context, uri, null, sql, selectionArgs, null);
	}

	/**
	 * 模仿android的凑sql的方法，不同在于支持join和on，用在raw-query
	 *
	 * @param projection
	 * @param selection
	 * @param from
	 * @param joinOn     自己写上什么类型的join和on关键字(right, full not support)
	 * @param sort
	 * @param limit
	 * @return query sql
	 */
	public static String buildQuery(String[] projection, String selection, String from, String joinOn, String sort, String limit) {
		StringBuilder query = new StringBuilder("SELECT");
		if (projection == null) {
			query.append(" * ");
		} else {
			query.append(" ").append(projection(projection));
		}

		query.append(" FROM ").append(from);

		if (joinOn != null) {
			// 至于是什么类型的join，多少个join和on，这里得自己写上
			query.append(" ").append(joinOn);
		}

		if (selection != null) {
			query.append(" WHERE ").append(selection);
		}

		if (sort != null) {
			query.append(" ORDER BY ").append(sort);
		}

		if (limit != null) {
			query.append(" LIMIT ").append(limit);
		}

		return query.toString();
	}

	/**
	 * 给字符串首位加上单引号
	 */
	public static String quote(String string) {
		return "'" + string + "'";
	}

	/**
	 * 拼接投影sql
	 *
	 * @param projection
	 * @return turn a string[] into xx,xxx,xxx,...,xx
	 */
	private static String projection(String[] projection) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < projection.length; i++) {
			sb.append(projection[i]);
			if (i != projection.length - 1) {
				sb.append(",");
			}
		}
		return sb.toString();
	}

	/**
	 * 更新sql
	 */
	public static String update(ContentValues values, String from, String where) {
		StringBuilder sb = new StringBuilder("UPDATE ").append(from).append(" SET");
		for (String key : values.keySet()) {
			sb.append(" ").append(key).append("=").append(reflectString(values.get(key))).append(",");
		}
		sb.deleteCharAt(sb.length() - 1); // remove the last ','
		if (where != null) {
			sb.append(" WHERE ").append(where);
		}
		return sb.toString();
	}

	/**
	 * 对某个字段自增或者自减法，只能通过_id=?来唯一确定哦
	 *
	 * @param increment
	 * @param table
	 * @param column
	 * @param _id
	 * @return update sql
	 */
	public static String increment(boolean increment, String table, String column, long _id) {
		return new StringBuilder("UPDATE ")
				.append(table).append(" ").append(" SET ")
				.append(column).append("=").append(column)
				.append(increment ? "+1" : "-1")
				.append(" WHERE ").append(BaseColumns._ID)
				.append("=").append(_id)
				.toString();
	}

	private static String reflectString(Object object) {
		if (object instanceof CharSequence) {
			return quote(object.toString());
		} else {
			// 其它情况下一律认为是number类型
			return object.toString();
		}
	}

	/**
	 * simple wrap the keywords with '%xx%'
	 *
	 * @param keywords
	 * @return '%keywords%'
	 */
	public static String like(String keywords) {
		return "'%" + keywords + "%'";
	}

	/**
	 * 给微博文字加上样式，表情，##，@，链接等
	 *
	 * @param text      微博
	 * @param imageSpan 不需要表情传入null
	 */
	public static void vividTweet(TweetTextView text, TweetImageSpan imageSpan) {
		if (imageSpan != null) {
			text.setText(imageSpan.getImageSpan(text.getText()));
		}
		Linkify.addLinks(text, MENTION_PATTERN, MENTION_SCHEME, null, MENTION_FILTER);
		Linkify.addLinks(text, TOPIC_PATTERN, TOPIC_SCHEME, null, TOPIC_FILTER);
		Linkify.addLinks(text, WEB_URL, null, null, URL_FILTER);
		removeLinkUnderline(text);
	}

	/**
	 * 清除textview链接的下划线
	 *
	 * @param textView
	 */
	public static void removeLinkUnderline(TextView textView) {
		Spannable s = Spannable.Factory.getInstance().newSpannable(textView.getText());
		URLSpan[] spans = s.getSpans(0, s.length(), URLSpan.class);
		for (URLSpan span : spans) {
			int start = s.getSpanStart(span);
			int end = s.getSpanEnd(span);
			s.removeSpan(span);
			span = new TweetURLSpan(span.getURL());
			s.setSpan(span, start, end, 0);
		}
		textView.setText(s);
	}

	/**
	 * 当使用list pref时只能存储string array，so 这个方法你懂的
	 *
	 * @param pref
	 * @param key
	 * @param defaultValue
	 * @return key exists ? existed value : defaultValue
	 */
	public static int resolveListPrefInt(SharedPreferences pref, String key, int defaultValue) {
		String value = pref.getString(key, null);
		if (value == null) {
			return defaultValue;
		}
		return Integer.parseInt(value);
	}

	/**
	 * 简单的判断一下edit text的是否有输入，裁剪掉两边的空白
	 */
	public static boolean hasLength(EditText editText) {
		// text never null!
		String str = editText.getText().toString();
		if (!TextUtils.isEmpty(str)) {
			return str.trim().length() > 0;
		} else {
			return false;
		}
	}

	/**
	 * 微博文字转表情
	 *
	 * @param boundPx the icon' s rectangle bound, if zero, use the default
	 */
	public static SpannableString text2Emotion(Context context, String key, int boundPx) {
		SpannableString spannable = new SpannableString(key);
		InputStream inputStream = null;
		Drawable drawable = null;
		try {
			inputStream = context.getAssets().open(TweetImageSpan.EMOTIONS_DIR + TweetImageSpan.EMOTIONS.get(key));
			drawable = Drawable.createFromStream(inputStream, null);
		} catch (IOException e) {
			Log.e(TAG, "load emotion error!", e);
		} finally {
			closeIO(inputStream);
		}
		if (drawable != null) {
			if (boundPx == 0) {
				drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
			} else {
				drawable.setBounds(0, 0, boundPx, boundPx);
			}
			ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);
			spannable.setSpan(span, 0, key.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
		}
		return spannable;
	}

	/**
	 * get byte[] from input stream
	 *
	 * @param inputStream
	 * @return byte[]
	 * @throws IOException
	 */
	public static byte[] getBytes(InputStream inputStream) throws IOException {
		ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
		int bufferSize = 2048 * 10; // 2m
		byte[] buffer = new byte[bufferSize];

		int len;
		while ((len = inputStream.read(buffer)) != -1) {
			byteBuffer.write(buffer, 0, len);
		}
		return byteBuffer.toByteArray();
	}

	public static Uri createImageFile() {
		// Create an image file name
		String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
		String imageFileName = "JPEG_" + timeStamp + "_";
		File storageDir = Environment.getExternalStoragePublicDirectory(
				Environment.DIRECTORY_PICTURES);
		File image;
		try {
			image = File.createTempFile(
					imageFileName,  /* prefix */
					".jpg",         /* suffix */
					storageDir      /* directory */
			);
		} catch (IOException e) {
			Log.d(TAG, "create tmp file error!", e);
			return null;
		}
		return Uri.fromFile(image);
	}

	// 是否需要更新
	public static void checkout(boolean required, Context context, SharedPreferences preferences) {
		long now = System.currentTimeMillis();
		String key = context.getString(R.string.pref_check_upgrade);
		if (required) {
			context.startService(new Intent(context, UpgradeService.class));
			preferences.edit().putLong(key, now).commit();
		} else {
			long last = preferences.getLong(key, now);
			if (now - last > (7 * 24 * 60 * 60 * 1000)) { // 每周更新一次
				Log.d(TAG, "need upgrade...");
				preferences.edit().putLong(key, now).commit();
				context.startService(new Intent(context, UpgradeService.class));
			}
		}
	}

	/**
	 * 根据偏好中保存的文件路径获取自定义字体
	 *
	 * @param pref
	 * @param key
	 * @param defaultTypeface
	 * @return 若发生任何异常或者和默认相同则返回null
	 */
	public static Typeface getTypeface(SharedPreferences pref, String key, String defaultTypeface) {
		Typeface typeface = null;
		String fontPath = pref.getString(key, null);
		if (fontPath == null || fontPath.equals(defaultTypeface)) {
			return null;
		} else {
			try {
				typeface = Typeface.createFromFile(new File(fontPath));
			} catch (Exception e) {
			}
		}
		return typeface;
	}

	/**
	 * helper method to check whether the typeface is null or not.
	 *
	 * @param textView
	 * @param typeface
	 */
	public static void setTypeface(TextView textView, Typeface typeface) {
		if (typeface != null) {
			textView.setTypeface(typeface);
		}
	}

	/**
	 * 设置行间距
	 *
	 * @param preferences
	 * @param key
	 * @param defaultValue
	 */
	public static float getLineSpacing(SharedPreferences preferences, String key, String defaultValue) {
		return Float.parseFloat(preferences.getString(key, defaultValue));
	}

	public static String mkdir(Context context, String location) throws Exception {
		// 注意，这里用户有可能吧sk卡连接到电脑上了...
		File dir = new File(context.getExternalCacheDir().getPath()
				+ File.separator + location);
		if (!dir.exists() && !dir.mkdirs()) {
			throw new RuntimeException(context.getString(R.string.cannot_create_dir));
		}
		return dir.getPath();
	}

	// 关掉io
	public static void closeIO(Closeable... closeables) {
		for (Closeable closeable : closeables) {
			if (closeable != null) {
				try {
					closeable.close();
				} catch (IOException e) {
					// no-op
				}
			}
		}
	}

	/**
	 * copy plain text to clip board
	 *
	 * @param context
	 * @param tag
	 * @param text
	 * @param toastText may null, no toast at all
	 */
	public static void copy2ClipBoard(Context context, String tag, String text, String toastText) {
		ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
		ClipData clip = ClipData.newPlainText(tag, text);
		clipboard.setPrimaryClip(clip);
		if (toastText != null) {
			Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
		}
	}

	/**
	 * 检查激活了那些插件
	 *
	 * @return plug' s id, or null
	 */
	public static ArrayList<Integer> enabledPlugins() {
		ArrayList<Integer> ids = new ArrayList<Integer>();
		if (CatnutApp.getBoolean(R.string.pref_enable_zhihu,R.bool.default_plugin_status)) {
			ids.add(PluginsPrefFragment.ZHIHU);
		}
		if (CatnutApp.getBoolean(R.string.pref_enable_fantasy, R.bool.pref_enable_fantasy)) {
			ids.add(PluginsPrefFragment.FANTASY);
		}
		return ids.size() == 0 ? null : ids;
	}

	public static int getScreenWidth(Context context) {
		DisplayMetrics metrics = context.getResources().getDisplayMetrics();
		return metrics.widthPixels;
	}

	/**
	 * inject a image touch overley
	 * @param v
	 * @param event
	 */
	public static boolean imageOverlay(View v, MotionEvent event) {
		ImageView view = (ImageView) v;
		switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				// overlay is black with transparency of 0x77 (119)
				view.getDrawable().setColorFilter(0x77000000, PorterDuff.Mode.SRC_ATOP);
				view.invalidate();
				break;
			case MotionEvent.ACTION_UP:
			case MotionEvent.ACTION_CANCEL: {
				// clear the overlay
				view.getDrawable().clearColorFilter();
				view.invalidate();
				break;
			}
		}
		return false;
	}

	/**
	 * if pics.len > 1, return the json, or null
	 * @param json
	 * @return
	 */
	public static JSONArray optPics(String json) {
		if (!TextUtils.isEmpty(json)) {
			try {
				JSONArray jsonArray = new JSONArray(json);
				return optPics(jsonArray);
			} catch (JSONException e) {
				Log.wtf(TAG, "cannot serialize pics json array!", e);
				return null;
			}
		}
		return null;
	}

	/**
	 * if pics.len > 1, return the json, or null
	 * @param jsonArray
	 * @return
	 */
	public static JSONArray optPics(JSONArray jsonArray) {
		return jsonArray == null ? null : jsonArray.length() > 1 ? jsonArray : null;
	}
} public class Constants {

	public static final String WEIBO_DOMAIN = "weibo.com/";

	public static final String ACTION = "action";
	public static final String ID = "id";
	public static final String PIC = "pic";
	public static final String THUMBNAIL = "thumbnail";
	public static final String IMAGE_DIR = "images/"; // 保持在用户外部存储上的，并非cache的
	public static final String FANTASY_DIR = "fantasy";
	public static final String NULL = "null";
	public static final String JPG = ".jpg";
	public static final String JSON = "json";
	public static final String KEYWORDS = "keywords";

	public static final float GOLDEN_RATIO = 0.618f;

	public static final String SHARE_IMAGE = "share.png";

	// default sort order, must have `_id` field!
	public static final String DEFAULT_ORDER = BaseColumns._ID + " desc";
	public static final String RANDOM_ORDER = "RANDOM()";

	public static final String[] COUNT_PROJECTION = new String[]{
			"count(0)"
	};
} public class DateTime {

	private static final String TAG = "DateTime";

	public static final int SECOND_MILLIS = 1000;
	public static final int MINUTE_MILLIS = 60 * SECOND_MILLIS;
	public static final int HOUR_MILLIS = 60 * MINUTE_MILLIS;
	public static final long DAY_MILLIS = 24 * HOUR_MILLIS;
	public static final long MONTH_MILLIS = 30 * DAY_MILLIS; // 30 days as a month
	public static final long YEAR_MILLIS = 12 * MONTH_MILLIS;

	private static final int[] CONVERSIONS = {
			1,		// ms, now
			1000,	// ms, sec
			60,		// sec, min
			60,		// min, hour
			24,		// hour, day
			30,		// day, month
			12,		// month, year
	};

	private static final String[] SPANS = {
			"now",
			"s",
			"m",
			"h",
			"d",
			"mon",
			"year",
	};

	/**
	 * 将微博文本转换为时间
	 *
	 * @return 若失败，返回null
	 */
	public static Date parse(String string) {
		try {
			return sSafeDateFormat.get().parse(string);
		} catch (ParseException e) {
			Log.e(TAG, "parsing string [" + string + "] to date error!", e);
			return null;
		}
	}

	/**
	 * 将文本转换为时间
	 *
	 * @return 若失败，返回0
	 */
	public static long getTimeMills(String string) {
		try {
			return sSafeDateFormat.get().parse(string).getTime();
		} catch (ParseException e) {
			Log.e(TAG, "parsing string [" + string + "] to millis error!", e);
			return 0L;
		}
	}

	/**
	 * simplicity time
	 */
	public static String getRelativeTimeString(String string) {
		long now = System.currentTimeMillis();
		long delta = now - getTimeMills(string);
		delta = Math.abs(delta);
		int unitKey = 0; // ms
		for (int i = 0; i < CONVERSIONS.length; i++) {
			if (delta < CONVERSIONS[i]) {
				break;
			}
			unitKey = i;
			delta /= CONVERSIONS[i];
		}
		return delta + SPANS[unitKey];
	}

	private static ThreadLocal<SimpleDateFormat> sSafeDateFormat = new ThreadSafeDateFormat();

	public static class ThreadSafeDateFormat extends ThreadLocal<SimpleDateFormat> {

		@Override
		protected SimpleDateFormat initialValue() {
			return new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy", Locale.ENGLISH);
		}

		@Override
		public SimpleDateFormat get() {
			return super.get();
		}

		@Override
		public void set(SimpleDateFormat value) {
			super.set(value);
		}

		@Override
		public void remove() {
			super.remove();
		}
	}
}
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri;
import android.provider.BaseColumns;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.viewpagerindicator.LinePageIndicator;
import org.catnut.R;
import org.catnut.api.FavoritesAPI;
import org.catnut.core.CatnutApp;
import org.catnut.core.CatnutRequest;
import org.catnut.fragment.GalleryPagerFragment;
import org.catnut.metadata.Status;
import org.catnut.metadata.User;
import org.catnut.metadata.WeiboAPIError;
import org.catnut.processor.StatusProcessor;
import org.catnut.support.PageTransformer;
import org.catnut.support.TouchImageView;
import org.catnut.support.TweetImageSpan;
import org.catnut.support.TweetTextView;
import org.catnut.support.annotation.NotNull;
import org.catnut.support.annotation.Nullable;
import org.catnut.ui.ProfileActivity;
import org.catnut.ui.SingleFragmentActivity;
import org.catnut.ui.TweetActivity;
import org.catnut.util.CatnutUtils;
import org.catnut.util.Constants;
import org.catnut.util.DateTime;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
						break;
					default:
						break;
				}
				return false;
			}
		});
		popup.show();
	}

	@Override
	public void onClick(final View v) {
		v.post(new Runnable() {
			@Override
			public void run() {
				showPopupMenu(v);
			}
		});
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
		if (!TextUtils.isEmpty(key)) {
			if (key.equals(mContext.getString(R.string.pref_thumbs_options))) {
				mThumbsOption = ThumbsOption.obtainOption(sharedPreferences.getString(
						key, mContext.getString(R.string.default_thumbs_option))
				);
				notifyDataSetChanged();
			} else if (key.equals(mContext.getString(R.string.pref_line_spacing))) {
				mCustomizedLineSpacing = CatnutUtils.getLineSpacing(
						sharedPreferences, key, mContext.getString(R.string.default_line_spacing)
				);
				notifyDataSetChanged();
			} else if (key.equals(mContext.getString(R.string.pref_tweet_font_size))) {
				mCustomizedFontSize = CatnutUtils.resolveListPrefInt(
						sharedPreferences,
						key,
						mContext.getResources().getInteger(R.integer.default_tweet_font_size)
				);
				notifyDataSetChanged();
			} else if (key.equals(mContext.getString(R.string.pref_customize_tweet_font))) {
				mCustomizedFont = CatnutUtils.getTypeface(
						sharedPreferences, key, mContext.getString(R.string.use_default_font)
				);
				notifyDataSetChanged();
			} else if (key.equals(mContext.getString(R.string.pref_keep_latest))) {
				mStayInLatest = sharedPreferences.getBoolean(key, true);
				// no need invalidating the existing view
			}
		}
	}

	@Override
	public View newView(Context context, Cursor cursor, ViewGroup parent) {
		ViewHolder holder = new ViewHolder();
		View view = mInflater.inflate(R.layout.tweet_row, null);
		holder.nick = (TextView) view.findViewById(R.id.nick);
		// 如果是某个主页时间线
		if (mScreenName == null) {
			holder.nickIndex = cursor.getColumnIndex(User.screen_name);
			holder.avatar = (ImageView) view.findViewById(R.id.avatar);
			holder.avatarIndex = cursor.getColumnIndex(User.avatar_large);
		}
		// 微博相关
		holder.text = (TweetTextView) view.findViewById(R.id.text);
		holder.textIndex = cursor.getColumnIndex(Status.columnText);
		holder.create_atIndex = cursor.getColumnIndex(Status.created_at);
		holder.replyCount = (TextView) view.findViewById(R.id.reply_count);
		holder.replyCountIndex = cursor.getColumnIndex(Status.comments_count);
		holder.reteetCount = (TextView) view.findViewById(R.id.reteet_count);
		holder.reteetCountIndex = cursor.getColumnIndex(Status.reposts_count);
		holder.likeCount = (TextView) view.findViewById(R.id.like_count);
		holder.likeCountIndex = cursor.getColumnIndex(Status.attitudes_count);
		holder.source = (TextView) view.findViewById(R.id.source);
		holder.smallThumbIndex = cursor.getColumnIndex(Status.thumbnail_pic);
		holder.mediumThumbIndex = cursor.getColumnIndex(Status.bmiddle_pic);
		holder.sourceIndex = cursor.getColumnIndex(Status.source);
		holder.originalPicIndex = cursor.getColumnIndex(Status.original_pic);
		holder.thumbs = (ImageView) view.findViewById(R.id.thumbs);
		holder.thumbs.setAdjustViewBounds(true);
		holder.picsOverflow = (ImageView) view.findViewById(R.id.pics_overflow);
		holder.picsIndex = cursor.getColumnIndex(Status.pic_urls);
		holder.remarkIndex = cursor.getColumnIndex(User.remark);
		holder.reply = (ImageView) view.findViewById(R.id.action_reply);
		holder.retweet = (ImageView) view.findViewById(R.id.action_reteet);
		// 转发
		holder.retweetView = view.findViewById(R.id.retweet);
		holder.retweetIndex = cursor.getColumnIndex(Status.retweeted_status);
		// time ``line``
		holder.time = (TextView) view.findViewById(R.id.time);
		holder.verified = view.findViewById(R.id.verified);
		holder.popup = view.findViewById(R.id.tweet_overflow);
		view.setTag(holder);
		return view;
	}

	@Override
	public void bindView(View view, final Context context, Cursor cursor) {
		final ViewHolder holder = (ViewHolder) view.getTag();
		final long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
		// 不是某个用户的时间线
		if (mScreenName == null) {
			holder.avatar.setVisibility(View.VISIBLE);
			RequestCreator creator = Picasso.with(context)
					.load(cursor.getString(holder.avatarIndex))
					.placeholder(R.drawable.ic_social_person_light);
			if (mStayInLatest) {
				creator.error(R.drawable.error);
			}
			creator.into(holder.avatar);
			// 跳转到该用户的时间线
			final String nick = cursor.getString(holder.nickIndex);
			final long uid = cursor.getLong(cursor.getColumnIndex(Status.uid));
			holder.avatar.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View v) {
					holder.avatar.getDrawable().clearColorFilter();
					holder.avatar.invalidate();
					Intent intent = new Intent(mContext, ProfileActivity.class);
					intent.putExtra(User.screen_name, nick);
					intent.putExtra(Constants.ID, uid);