public class FriendshipsAPI {

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

	/**
	 * 获取用户的关注列表
	 *
	 * @param uid         需要查询的用户UID
	 * @param count       单页返回的记录条数，默认为50，最大不超过200
	 * @param cursor      返回结果的游标，下一页用返回值里的next_cursor，上一页用previous_cursor，默认为0。
	 * @param trim_status 返回值中user字段中的status字段开关，0：返回完整status字段、1：status字段仅返回status_id，默认为1
	 * @return 用户的关注列表api
	 */
	public static CatnutAPI friends(long uid, int count, int cursor, int trim_status) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("friends.json")
				.append("?uid=").append(uid)
				.append("&count=").append(CatnutUtils.optValue(count, 50))
				.append("&cursor=").append(CatnutUtils.optValue(cursor, 0))
				.append("&trim_status=").append(CatnutUtils.optValue(trim_status, 1));
		return new CatnutAPI(Request.Method.GET, uri, true, null);
	}

	/**
	 * 获取用户的关注列表
	 *
	 * @param screen_name 需要查询的用户昵称。
	 * @param count       单页返回的记录条数，默认为50，最大不超过200
	 * @param cursor      返回结果的游标，下一页用返回值里的next_cursor，上一页用previous_cursor，默认为0。
	 * @param trim_status 返回值中user字段中的status字段开关，0：返回完整status字段、1：status字段仅返回status_id，默认为1
	 * @return 用户的关注列表api
	 */
	public static CatnutAPI friends(String screen_name, int count, int cursor, int trim_status) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("friends.json")
				.append("?screen_name=").append(CatnutAPI.encode(screen_name))
				.append("&count=").append(CatnutUtils.optValue(count, 50))
				.append("&cursor=").append(CatnutUtils.optValue(cursor, 0))
				.append("&trim_status=").append(CatnutUtils.optValue(trim_status, 1));
		return new CatnutAPI(Request.Method.GET, uri, true, null);
	}

	/**
	 * 获取用户的粉丝列表
	 *
	 * @param uid         需要查询的用户UID
	 * @param count       单页返回的记录条数，默认为50，最大不超过200
	 * @param cursor      返回结果的游标，下一页用返回值里的next_cursor，上一页用previous_cursor，默认为0
	 * @param trim_status 返回值中user字段中的status字段开关，0：返回完整status字段、1：status字段仅返回status_id，默认为1
	 * @return api
	 */
	public static CatnutAPI followers(long uid, int count, int cursor, int trim_status) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("followers.json")
				.append("?uid=").append(uid)
				.append("&count=").append(CatnutUtils.optValue(count, 50))
				.append("&cursor=").append(CatnutUtils.optValue(cursor, 0))
				.append("&trim_status=").append(CatnutUtils.optValue(trim_status, 1));
		return new CatnutAPI(Request.Method.GET, uri, true, null);
	}

	/**
	 * 获取用户的粉丝列表
	 *
	 * @param screen_name 需要查询的用户昵称。
	 * @param count       单页返回的记录条数，默认为50，最大不超过200
	 * @param cursor      返回结果的游标，下一页用返回值里的next_cursor，上一页用previous_cursor，默认为0
	 * @param trim_status 返回值中user字段中的status字段开关，0：返回完整status字段、1：status字段仅返回status_id，默认为1
	 * @return api
	 */
	public static CatnutAPI followers(String screen_name, int count, int cursor, int trim_status) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("followers.json")
				.append("?screen_name=").append(CatnutAPI.encode(screen_name))
				.append("&count=").append(CatnutUtils.optValue(count, 50))
				.append("&cursor=").append(CatnutUtils.optValue(cursor, 0))
				.append("&trim_status=").append(CatnutUtils.optValue(trim_status, 1));
		return new CatnutAPI(Request.Method.GET, uri, true, null);
	}

	/**
	 * 关注一个用户
	 *
	 * @param screen_name 需要关注的用户昵称
	 * @param rip         开发者上报的操作用户真实IP，形如：211.156.0.1
	 * @return api
	 */
	public static CatnutAPI create(String screen_name, String rip) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("create.json")
				.append("?screen_name=").append(CatnutAPI.encode(screen_name))
				.append("&rip=").append(rip);
		return new CatnutAPI(Request.Method.POST, uri, true, null);
	}

	/**
	 * 取消关注一个用户
	 *
	 * @param screen_name 需要取消关注的用户昵称
	 * @return api
	 */
	public static CatnutAPI destroy(String screen_name) {
		StringBuilder uri = new StringBuilder(BASE_URI);
		uri.append("destroy.json")
				.append("?screen_name=").append(CatnutAPI.encode(screen_name));
		return new CatnutAPI(Request.Method.POST, uri, true, null);
	}
} public class UserAPI {

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

	/**
	 * 根据用户ID获取用户信息
	 *
	 * @param uid 需要查询的用户ID
	 * @return api
	 */
	public static CatnutAPI profile(long uid) {
		return new CatnutAPI(
			Request.Method.GET,
			BASE_URI + "show.json?uid=" + uid,
			true,
			null
		);
	}

	/**
	 * 根据用户名获取用户信息
	 *
	 * @param screen_name 需要查询的用户昵称
	 * @return api
	 */
	public static CatnutAPI profile(String screen_name) {
		return new CatnutAPI(
				Request.Method.GET,
				BASE_URI + "show.json?screen_name=" + CatnutAPI.encode(screen_name),
				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 CatnutProvider extends ContentProvider {

	public static final String TAG = "CatnutProvider";
	private static final String CLEAR_DATA = "clear";

	public static final String AUTHORITY = "org.catnut";
	public static final String BASE_URI = "content://" + AUTHORITY;

	public static final String MULTIPLE_RECORDS_MIME_TYPE = "vnd.android.cursor.dir/vnd." + AUTHORITY + ".";
	public static final String SINGLE_RECORD_MIME_TYPE = "vnd.android.cursor.item/vnd." + AUTHORITY + ".";

	public static final int USER = 0;
	public static final int USERS = 1;
	public static final int STATUS = 2;
	public static final int STATUSES = 3;
	public static final int DRAFT = 4;
	public static final int DRAFTS = 5;
	public static final int PHOTO = 6;
	public static final int PHOTOS = 7;
	public static final int COMMENT = 8;
	public static final int COMMENTS = 9;

	// plugins...starting
	public static final int ZHIHU = 20;
	public static final int ZHIHUS = 21;
	// plugins...ending

	public static final int CLEAR = 100;

	private static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

	static {
		matcher.addURI(AUTHORITY, User.MULTIPLE + "/#", USER);
		matcher.addURI(AUTHORITY, User.MULTIPLE, USERS);
		matcher.addURI(AUTHORITY, Status.MULTIPLE + "/#", STATUS);
		matcher.addURI(AUTHORITY, Status.MULTIPLE, STATUSES);
		matcher.addURI(AUTHORITY, Draft.MULTIPLE + "/#", DRAFT);
		matcher.addURI(AUTHORITY, Draft.MULTIPLE, DRAFTS);
		matcher.addURI(AUTHORITY, Photo.MULTIPLE + "/#", PHOTO);
		matcher.addURI(AUTHORITY, Photo.MULTIPLE, PHOTOS);
		matcher.addURI(AUTHORITY, Comment.MULTIPLE + "/#", COMMENT);
		matcher.addURI(AUTHORITY, Comment.MULTIPLE, COMMENTS);

		// plugins...starting
		matcher.addURI(AUTHORITY, Zhihu.MULTIPLE + "/#", ZHIHU);
		matcher.addURI(AUTHORITY, Zhihu.MULTIPLE, ZHIHUS);
		// plugins...ending

		matcher.addURI(AUTHORITY, CLEAR_DATA, CLEAR);
	}

	/**
	 * 列表uri，如http://example.org/resources
	 *
	 * @param path 如 resources
	 * @return Uri
	 */
	public static Uri parse(String path) {
		return Uri.parse(BASE_URI + "/" + path);
	}

	/**
	 * 单个对象uri，如http://example.org/resources/1
	 *
	 * @param path 如 resources
	 * @param id
	 * @return Uri
	 */
	public static Uri parse(String path, long id) {
		return Uri.parse(BASE_URI + "/" + path + "/" + id);
	}

	/**
	 * 清除缓存数据
	 *
	 * @return uri
	 */
	public static Uri clear() {
		return Uri.parse(BASE_URI + File.separator + CLEAR_DATA);
	}

	/**
	 * 数据源实际持有的数据库连接
	 */
	private TingtingSource mDb;

	@Override
	public boolean onCreate() {
		mDb = new TingtingSource(getContext(), "catnut.db", 2);
		return true;
	}

	@Override
	public String getType(Uri uri) {
		String type = null;
		switch (matcher.match(uri)) {
			case USER:
				type = SINGLE_RECORD_MIME_TYPE + User.SINGLE;
				break;
			case USERS:
				type = MULTIPLE_RECORDS_MIME_TYPE + User.MULTIPLE;
				break;
			case STATUS:
				type = SINGLE_RECORD_MIME_TYPE + Status.SINGLE;
				break;
			case STATUSES:
				type = MULTIPLE_RECORDS_MIME_TYPE + Status.MULTIPLE;
				break;
			case DRAFT:
				type = SINGLE_RECORD_MIME_TYPE + Draft.SINGLE;
				break;
			case DRAFTS:
				type = MULTIPLE_RECORDS_MIME_TYPE + Draft.MULTIPLE;
				break;
			case PHOTO:
				type = SINGLE_RECORD_MIME_TYPE + Photo.SINGLE;
				break;
			case PHOTOS:
				type = MULTIPLE_RECORDS_MIME_TYPE + Photo.MULTIPLE;
				break;
			case COMMENT:
				type = SINGLE_RECORD_MIME_TYPE + Comment.SINGLE;
				break;
			case COMMENTS:
				type = MULTIPLE_RECORDS_MIME_TYPE + Comment.MULTIPLE;
				break;
			// plugins...starting
			case ZHIHU:
				type = SINGLE_RECORD_MIME_TYPE + Zhihu.SINGLE;
				break;
			case ZHIHUS:
				type = MULTIPLE_RECORDS_MIME_TYPE + Zhihu.MULTIPLE;
				break;
			// plugins...ending
			default:
				Log.wtf(TAG, "unknown uri: " + uri);
		}
		return type;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
		SQLiteDatabase db = mDb.getReadableDatabase();
		Cursor cursor;
		switch (matcher.match(uri)) {
			case USER:
				cursor = db.query(User.TABLE, projection, queryWithId(uri), selectionArgs, null, null, null);
				break;
			case USERS:
				cursor = db.rawQuery(selection, selectionArgs);
//				cursor = db.query(User.TABLE, projection, selection, selectionArgs, null, null, sortOrder);
				break;
			case STATUS:
				cursor = db.query(Status.TABLE, projection, queryWithId(uri), selectionArgs, null, null, null);
				break;
			case STATUSES:
				// 这里，比较特殊，直接在selection中写sql，可以包含占位符
				cursor = db.rawQuery(selection, selectionArgs);
				break;
			case DRAFT:
			case DRAFTS:
				cursor = db.query(Draft.TABLE, projection, selection, selectionArgs, selection, null, sortOrder);
				break;
			case PHOTO:
			case PHOTOS:
				cursor = db.rawQuery(selection, selectionArgs);
				break;
			case COMMENT:
				// fall through currently
			case COMMENTS:
				cursor = db.rawQuery(selection, selectionArgs);
				break;
			// plugins...starting
			case ZHIHU:
				cursor = db.query(Zhihu.TABLE, projection, queryWithId(uri), selectionArgs, null, null, sortOrder);
				break;
			case ZHIHUS:
				cursor = db.query(Zhihu.TABLE, projection, selection, selectionArgs, null, null, sortOrder);
				break;
			// plugins...ending
			default:
				Log.wtf(TAG, "unknown uri: " + uri);
				return null;
		}
		cursor.setNotificationUri(getContext().getContentResolver(), uri);
		return cursor;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		String table;
		switch (matcher.match(uri)) {
			case USERS:
				table = User.TABLE;
				break;
			case STATUSES:
				table = Status.TABLE;
				break;
			case DRAFTS:
				table = Draft.TABLE;
				break;
			case PHOTOS:
				table = Photo.TABLE;
				break;
			case COMMENTS:
				table = Comment.TABLE;
				break;
			// plugins...starting
			case ZHIHUS:
				table = Zhihu.TABLE;
				break;
			// plugins...ending
			default:
				throw new RuntimeException("unknown uri: " + uri);
		}
		SQLiteDatabase db = mDb.getWritableDatabase();
		long id = db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
		getContext().getContentResolver().notifyChange(uri, null, false); // no syncAdapter
		return ContentUris.withAppendedId(uri, id);
	}

	@Override
	public int bulkInsert(Uri uri, ContentValues[] values) {
		int conflicOption = SQLiteDatabase.CONFLICT_REPLACE;
		String table;
		switch (matcher.match(uri)) {
			case USERS:
				table = User.TABLE;
				break;
			case STATUSES:
				table = Status.TABLE;
				break;
			case PHOTOS:
				table = Photo.TABLE;
				break;
			case COMMENTS:
				table = Comment.TABLE;
				break;
			// plugins...starting
			case ZHIHUS:
				table = Zhihu.TABLE;
				conflicOption = SQLiteDatabase.CONFLICT_IGNORE;
				break;
			// plugins...ending
			default:
				throw new RuntimeException("unknown uri: " + uri);
		}
		SQLiteDatabase db = mDb.getWritableDatabase();
		db.beginTransaction();
		for (int i = 0; i < values.length; i++) {
			if (values[i] != null) {
				db.insertWithOnConflict(table, null, values[i], conflicOption);
			}
		}
		db.setTransactionSuccessful();
		db.endTransaction();
		getContext().getContentResolver().notifyChange(uri, null, false);
		return values.length;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		int count;
		switch (matcher.match(uri)) {
			case STATUSES:
				count = mDb.getWritableDatabase().delete(Status.TABLE, selection, selectionArgs);
				break;
			case DRAFTS:
				count = mDb.getWritableDatabase().delete(Draft.TABLE, selection, selectionArgs);
				break;
			case PHOTOS:
				count = mDb.getWritableDatabase().delete(Photo.TABLE, selection, selectionArgs);
				break;
			case COMMENTS:
				count = mDb.getWritableDatabase().delete(Comment.TABLE, selection, selectionArgs);
				break;
			// plugins...starting
			case ZHIHUS:
				// 直接清掉全部
				count = mDb.getWritableDatabase().delete(Zhihu.TABLE, null, null);
				break;
			// plugins...ending
			case CLEAR: // 插件的缓存清除分开搞
				SQLiteDatabase db = mDb.getWritableDatabase();
				db.beginTransaction();
				db.execSQL("delete from " + Photo.TABLE);
				db.execSQL("delete from " + User.TABLE + " where "
						+ BaseColumns._ID + " != " + CatnutApp.getTingtingApp().getAccessToken().uid);
				db.execSQL("delete from " + Status.TABLE);
				db.execSQL("delete from " + Comment.TABLE);
				db.setTransactionSuccessful();
				db.endTransaction();
				count = Integer.MAX_VALUE;
				break;
			default:
				throw new UnsupportedOperationException("not supported for now!");
		}
		getContext().getContentResolver().notifyChange(uri, null, false);
		return count;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
		int count = 1; // 随便填的
		SQLiteDatabase db;
		switch (matcher.match(uri)) {
			case USERS:
			case STATUSES:
				db = mDb.getWritableDatabase();
				db.execSQL(selection);
				break;
			// plugins...starting
			case ZHIHU:
				selection = Zhihu.ANSWER_ID + "=" + uri.getLastPathSegment();
				count = mDb.getWritableDatabase().update(Zhihu.TABLE, values, selection, selectionArgs);
				break;
			// plugins...ending
			default:
				throw new UnsupportedOperationException("not supported for now!");
		}
		getContext().getContentResolver().notifyChange(uri, null, false);
		return count;
	}

	/**
	 * 直接以id查询记录
	 *
	 * @param uri
	 * @return "_id=?"
	 */
	private static String queryWithId(Uri uri) {
		return "_id=" + uri.getLastPathSegment();
	}

	/**
	 * sqlite数据库
	 *
	 * @author longkai
	 */
	private static class TingtingSource extends SQLiteOpenHelper {

		private static final String TAG = "TingtingSource";

		private Context mContext;

		public TingtingSource(Context context, String name, int version) {
			super(context, name, null, version);
			mContext = context;
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			db.execSQL(User.METADATA.ddl());
			db.execSQL(Status.METADATA.ddl());
			db.execSQL(Draft.METADATA.ddl());
			db.execSQL(Photo.METADATA.ddl());
			db.execSQL(Comment.METADATA.ddl());
			// plugins...starting
			db.execSQL(Zhihu.METADATA.ddl());
			// plugins...ending

			// 保存分享的图片
			(new Thread(new Runnable() {
				@Override
				public void run() {
					Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher);
					FileOutputStream shareImage = null;
					try {
						shareImage = new FileOutputStream(mContext.getExternalCacheDir() + File.separator + Constants.SHARE_IMAGE);
						bitmap.compress(Bitmap.CompressFormat.PNG, 100, shareImage);
					} catch (FileNotFoundException e) {
					} finally {
						if (shareImage != null) {
							try {
								shareImage.close();
							} catch (IOException e) {
							}
						}
					}
				}
			})).start();
			Log.i(TAG, "finish create tables...");
		}

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
			Log.i(TAG, "drop tables...");
			db.execSQL("DROP TABLE IF EXISTS " + User.TABLE);
			db.execSQL("DROP TABLE IF EXISTS " + Status.TABLE);
			db.execSQL("DROP TABLE IF EXISTS " + Draft.TABLE);
			db.execSQL("DROP TABLE IF EXISTS " + Photo.TABLE);
			db.execSQL("DROP TABLE IF EXISTS " + Comment.TABLE);

			// plugins...starting
			db.execSQL("DROP TABLE IF EXISTS " + Zhihu.TABLE);
			// plugins...ending

			// recreate...
			onCreate(db);
			Log.i(TAG, "finish upgrade table...");
		}
	}
} 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 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 UserProcessor {

	/**
	 * 将我的好友持久化到本地
	 */
	public static class UsersProcessor implements CatnutProcessor<JSONObject> {

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			JSONArray jsonArray = data.optJSONArray(User.MULTIPLE);
			ContentValues[] users = new ContentValues[jsonArray.length()];
			// 可能会包含有这条用户最新的微博存在
			ContentValues[] tweets = null;
			boolean hasTweet = jsonArray.optJSONObject(0).has(Status.SINGLE);
			if (hasTweet) {
				tweets = new ContentValues[users.length];
			}

			for (int i = 0; i < users.length; i++) {
				JSONObject user = jsonArray.optJSONObject(i);
				users[i] = User.METADATA.convert(user);
				if (hasTweet) {
					tweets[i] = Status.METADATA.convert(user.optJSONObject(Status.SINGLE));
				}
			}

			context.getContentResolver().bulkInsert(CatnutProvider.parse(User.MULTIPLE), users);
			if (hasTweet) {
				context.getContentResolver().bulkInsert(CatnutProvider.parse(Status.MULTIPLE), tweets);
			}
		}
	}

	/**
	 * 持久化单个用户的信息
	 */
	public static class UserProfileProcessor implements CatnutProcessor<JSONObject> {

		@Override
		public void asyncProcess(Context context, JSONObject data) throws Exception {
			ContentValues user = User.METADATA.convert(data);
			// 如果包含了用户最新的一条微博
			if (data.has(Status.SINGLE)) {
				JSONObject json = data.getJSONObject(Status.SINGLE);
				ContentValues tweet = Status.METADATA.convert(json);
				// 注意，新浪此时返回的内联微博没有uid这项！
				tweet.put(Status.uid, data.optLong(Constants.ID));
				context.getContentResolver().insert(CatnutProvider.parse(Status.MULTIPLE), tweet);
			}
			context.getContentResolver().insert(CatnutProvider.parse(User.MULTIPLE), user);
		}
	}
} public class QuickReturnScrollView extends ScrollView {
	private Callbacks callbacks;

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

	@Override
	protected void onScrollChanged(int l, int t, int oldl, int oldt) {
		super.onScrollChanged(l, t, oldl, oldt);
		if (callbacks != null) {
			callbacks.onScrollChanged(t);
		}
	}

	@Override
	public int computeVerticalScrollRange() {
		return super.computeVerticalScrollRange();
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (callbacks != null) {
			switch (ev.getActionMasked()) {
				case MotionEvent.ACTION_DOWN:
					callbacks.onDownMotionEvent();
					break;
				case MotionEvent.ACTION_UP:
					// fall through
				case MotionEvent.ACTION_CANCEL:
					callbacks.onUpOrCancelMotionEvent();
					break;
				default:
					break;
			}
		}
		return super.onTouchEvent(ev);
	}

	public void setCallbacks(Callbacks callbacks) {
		this.callbacks = callbacks;
	}

	public static interface Callbacks {
		int STATE_ON_SCREEN = 0;
		int STATE_OFF_SCREEN = 1;
		int STATE_RETURNING = 2;

		void onScrollChanged(int scrollY);

		void onDownMotionEvent();

		void onUpOrCancelMotionEvent();
	}
} 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.Activity;
import android.app.Fragment;
import android.content.AsyncQueryHandler;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
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.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import com.viewpagerindicator.LinePageIndicator;
import org.catnut.R;
import org.catnut.api.FriendshipsAPI;
import org.catnut.api.UserAPI;
import org.catnut.core.CatnutApp;
import org.catnut.core.CatnutProvider;
import org.catnut.core.CatnutRequest;
import org.catnut.metadata.Status;
import org.catnut.metadata.User;
import org.catnut.processor.UserProcessor;
import org.catnut.support.QuickReturnScrollView;
import org.catnut.support.TweetImageSpan;
import org.catnut.support.TweetTextView;
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.JSONObject;
/*
 * The MIT License (MIT)
 * Copyright (c) 2014 longkai
 * The software shall be used for good, not evil.
 */
package org.catnut.fragment;


/**
 * 用户信息界面
 *
 * @author longkai
 */
public class ProfileFragment extends Fragment implements
		SharedPreferences.OnSharedPreferenceChangeListener, QuickReturnScrollView.Callbacks {

	private static final String[] PROJECTION = new String[]{
			BaseColumns._ID,
			User.remark,
			User.verified,
			User.screen_name,
			User.profile_url,
			User.location,
			User.description,
			User.avatar_large,
			User.avatar_hd,
			User.cover_image,
			User.favourites_count,
			User.friends_count,
			User.statuses_count,
			User.followers_count,
			User.following,
			User.verified_reason
	};

	private CatnutApp mApp;
	private Menu mMenu;

	private ScrollSettleHandler mScrollSettleHandler = new ScrollSettleHandler();
	private QuickReturnScrollView mQuickReturnScrollView;
	private View mQuickReturnView;
	private View mQuickReturnPlaceHolderView;
	private int mMinRawY = 0;
	private int mState = STATE_ON_SCREEN;
	private int mQuickReturnHeight;
	private int mMaxScrollY;

	private long mUid;
	private String mScreenName;
	private String mCoverUrl;
	private String mAvatarUrl;
	private String mAvatarHdUrl;
	private boolean mVerified;
	private boolean mFollowing; // 哥是否关注他
	private String mRemark;
	private String mDescription;
	private String mLocation;
	private String mProfileUrl;
	private String mVerifiedReason;

	private ViewPager mViewPager;
	private LinePageIndicator mIndicator;
	private View mPlaceHolder;
	private View mTweetsCount;
	private View mFollowingsCount;
	private View mFollowersCount;
	private View mTweetLayout;
	private View mRetweetLayout;

	private Typeface mTypeface;
	private float mLineSpacing = 1.0f;

	private View.OnClickListener tweetsOnclickListener = new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			ProfileActivity activity = (ProfileActivity) getActivity();
			activity.flipCard(UserTimelineFragment.getFragment(mUid, mScreenName), null, true);
		}
	};

	private View.OnClickListener followersOnclickListener = new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			ProfileActivity activity = (ProfileActivity) getActivity();
			activity.flipCard(TransientUsersFragment.getFragment(mScreenName, false), null, true);
		}
	};

	private View.OnClickListener followingsOnClickListener = new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			ProfileActivity activity = (ProfileActivity) getActivity();
			activity.flipCard(TransientUsersFragment.getFragment(mScreenName, true), null, true);
		}
	};

	private Target profileTarget = new Target() {
		@Override
		public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
			try {
				mPlaceHolder.setBackground(new BitmapDrawable(getResources(), bitmap));
			} catch (Exception e) {
				// no-op
			}
		}

		@Override
		public void onBitmapFailed(Drawable errorDrawable) {
			try {
				mPlaceHolder.setBackground(errorDrawable);
			} catch (Exception e) {
				// no-op
			}
		}

		@Override
		public void onPrepareLoad(Drawable placeHolderDrawable) {
			mPlaceHolder.setBackground(placeHolderDrawable);
		}
	};

	public static ProfileFragment getFragment(long uid, String screenName) {
		Bundle args = new Bundle();
		args.putLong(Constants.ID, uid);
		args.putString(User.screen_name, screenName);
		ProfileFragment fragment = new ProfileFragment();
		fragment.setArguments(args);
		return fragment;
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		mApp = CatnutApp.getTingtingApp();
		Bundle args = getArguments();
		mUid = args.getLong(Constants.ID);
		mScreenName = args.getString(User.screen_name);
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setHasOptionsMenu(true);
		SharedPreferences preferences = mApp.getPreferences();
		mTypeface = CatnutUtils.getTypeface(