/* * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.ch_linghu.fanfoudroid.ui.base; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import com.ch_linghu.fanfoudroid.R; import com.ch_linghu.fanfoudroid.TwitterApplication; import com.ch_linghu.fanfoudroid.app.Preferences; import com.ch_linghu.fanfoudroid.data.Tweet; import com.ch_linghu.fanfoudroid.db.StatusTable; import com.ch_linghu.fanfoudroid.fanfou.IDs; import com.ch_linghu.fanfoudroid.fanfou.Status; import com.ch_linghu.fanfoudroid.http.HttpException; import com.ch_linghu.fanfoudroid.task.GenericTask; import com.ch_linghu.fanfoudroid.task.TaskAdapter; import com.ch_linghu.fanfoudroid.task.TaskListener; import com.ch_linghu.fanfoudroid.task.TaskManager; import com.ch_linghu.fanfoudroid.task.TaskParams; import com.ch_linghu.fanfoudroid.task.TaskResult; import com.ch_linghu.fanfoudroid.ui.module.FlingGestureListener; import com.ch_linghu.fanfoudroid.ui.module.MyActivityFlipper; import com.ch_linghu.fanfoudroid.ui.module.SimpleFeedback; import com.ch_linghu.fanfoudroid.ui.module.TweetAdapter; import com.ch_linghu.fanfoudroid.ui.module.TweetCursorAdapter; import com.ch_linghu.fanfoudroid.util.DateTimeHelper; import com.ch_linghu.fanfoudroid.util.DebugTimer; import com.hlidskialf.android.hardware.ShakeListener; import com.markupartist.android.widget.PullToRefreshListView; import com.markupartist.android.widget.PullToRefreshListView.OnRefreshListener; /** * TwitterCursorBaseLine用于带有静态数据来源(对应数据库的,与twitter表同构的特定表)的展现 */ public abstract class TwitterCursorBaseActivity extends TwitterListBaseActivity { static final String TAG = "TwitterCursorBaseActivity"; // Views. protected PullToRefreshListView mTweetList; protected TweetCursorAdapter mTweetAdapter; protected View mListHeader; protected View mListFooter; protected TextView loadMoreBtn; protected ProgressBar loadMoreGIF; protected TextView loadMoreBtnTop; protected ProgressBar loadMoreGIFTop; protected static int lastPosition = 0; protected ShakeListener mShaker = null; // Tasks. protected TaskManager taskManager = new TaskManager(); private GenericTask mRetrieveTask; private GenericTask mFollowersRetrieveTask; private GenericTask mGetMoreTask; private int mRetrieveCount = 0; private TaskListener mRetrieveTaskListener = new TaskAdapter() { @Override public String getName() { return "RetrieveTask"; } @Override public void onPostExecute(GenericTask task, TaskResult result) { // 刷新按钮停止旋转 loadMoreGIF.setVisibility(View.GONE); mTweetList.onRefreshComplete(); if (result == TaskResult.AUTH_ERROR) { mFeedback.failed("登录信息出错"); logout(); } else if (result == TaskResult.OK) { // TODO: XML处理, GC压力 SharedPreferences.Editor editor = getPreferences().edit(); editor.putLong(Preferences.LAST_TWEET_REFRESH_KEY, DateTimeHelper.getNowTime()); editor.commit(); // TODO: 1. StatusType(DONE) ; if (mRetrieveCount >= StatusTable.MAX_ROW_NUM) { // 只有在取回的数据大于MAX时才做GC, 因为小于时可以保证数据的连续性 getDb().gc(getUserId(), getDatabaseType()); // GC } draw(); if (task == mRetrieveTask) { goTop(); } } else if (result == TaskResult.IO_ERROR) { // FIXME: bad smell if (task == mRetrieveTask) { mFeedback.failed(((RetrieveTask) task).getErrorMsg()); } else if (task == mGetMoreTask) { mFeedback.failed(((GetMoreTask) task).getErrorMsg()); } } else { // do nothing } // DEBUG if (TwitterApplication.DEBUG) { DebugTimer.stop(); Log.v("DEBUG", DebugTimer.getProfileAsString()); } } @Override public void onPreExecute(GenericTask task) { mRetrieveCount = 0; mTweetList.prepareForRefresh(); if (TwitterApplication.DEBUG) { DebugTimer.start(); } } @Override public void onProgressUpdate(GenericTask task, Object param) { Log.d(TAG, "onProgressUpdate"); draw(); } }; private TaskListener mFollowerRetrieveTaskListener = new TaskAdapter() { @Override public String getName() { return "FollowerRetrieve"; } @Override public void onPostExecute(GenericTask task, TaskResult result) { if (result == TaskResult.OK) { SharedPreferences sp = getPreferences(); SharedPreferences.Editor editor = sp.edit(); editor.putLong(Preferences.LAST_FOLLOWERS_REFRESH_KEY, DateTimeHelper.getNowTime()); editor.commit(); } else { // Do nothing. } } }; // Refresh data at startup if last refresh was this long ago or greater. private static final long REFRESH_THRESHOLD = 5 * 60 * 1000; // Refresh followers if last refresh was this long ago or greater. private static final long FOLLOWERS_REFRESH_THRESHOLD = 12 * 60 * 60 * 1000; abstract protected void markAllRead(); abstract protected Cursor fetchMessages(); public abstract int getDatabaseType(); public abstract String getUserId(); public abstract String fetchMaxId(); public abstract String fetchMinId(); public abstract int addMessages(ArrayList<Tweet> tweets, boolean isUnread); public abstract List<Status> getMessageSinceId(String maxId) throws HttpException; public abstract List<Status> getMoreMessageFromId(String minId) throws HttpException; public static final int CONTEXT_REPLY_ID = Menu.FIRST + 1; // public static final int CONTEXT_AT_ID = Menu.FIRST + 2; public static final int CONTEXT_RETWEET_ID = Menu.FIRST + 3; public static final int CONTEXT_DM_ID = Menu.FIRST + 4; public static final int CONTEXT_MORE_ID = Menu.FIRST + 5; public static final int CONTEXT_ADD_FAV_ID = Menu.FIRST + 6; public static final int CONTEXT_DEL_FAV_ID = Menu.FIRST + 7; @Override protected void setupState() { Cursor cursor; cursor = fetchMessages(); // getDb().fetchMentions(); setTitle(getActivityTitle()); startManagingCursor(cursor); mTweetList = (PullToRefreshListView) findViewById(R.id.tweet_list); // TODO: 需处理没有数据时的情况 Log.d("LDS", cursor.getCount() + " cursor count"); setupListHeader(true); mTweetAdapter = new TweetCursorAdapter(this, cursor); mTweetList.setAdapter(mTweetAdapter); // ? registerOnClickListener(mTweetList); } /** * 绑定listView底部 - 载入更多 NOTE: 必须在listView#setAdapter之前调用 */ protected void setupListHeader(boolean addFooter) { // Add Header to ListView //mListHeader = View.inflate(this, R.layout.listview_header, null); //mTweetList.addHeaderView(mListHeader, null, true); mTweetList.setOnRefreshListener(new OnRefreshListener(){ @Override public void onRefresh(){ doRetrieve(); } }); // Add Footer to ListView mListFooter = View.inflate(this, R.layout.listview_footer, null); mTweetList.addFooterView(mListFooter, null, true); // Find View loadMoreBtn = (TextView) findViewById(R.id.ask_for_more); loadMoreGIF = (ProgressBar) findViewById(R.id.rectangleProgressBar); loadMoreBtnTop = (TextView) findViewById(R.id.ask_for_more_header); loadMoreGIFTop = (ProgressBar) findViewById(R.id.rectangleProgressBar_header); } @Override protected void specialItemClicked(int position) { // 注意 mTweetAdapter.getCount 和 mTweetList.getCount的区别 // 前者仅包含数据的数量(不包括foot和head),后者包含foot和head // 因此在同时存在foot和head的情况下,list.count = adapter.count + 2 if (position == 0) { // 第一个Item(header) loadMoreGIFTop.setVisibility(View.VISIBLE); doRetrieve(); } else if (position == mTweetList.getCount() - 1) { // 最后一个Item(footer) loadMoreGIF.setVisibility(View.VISIBLE); doGetMore(); } } @Override protected int getLayoutId() { return R.layout.main; } @Override protected ListView getTweetList() { return mTweetList; } @Override protected TweetAdapter getTweetAdapter() { return mTweetAdapter; } @Override protected boolean useBasicMenu() { return true; } @Override protected Tweet getContextItemTweet(int position) { position = position - 1; // 因为List加了Header和footer,所以要跳过第一个以及忽略最后一个 if (position >= 0 && position < mTweetAdapter.getCount()) { Cursor cursor = (Cursor) mTweetAdapter.getItem(position); if (cursor == null) { return null; } else { return StatusTable.parseCursor(cursor); } } else { return null; } } @Override protected void updateTweet(Tweet tweet) { // TODO: updateTweet() 在哪里调用的? 目前尚只支持: // updateTweet(String tweetId, ContentValues values) // setFavorited(String tweetId, String isFavorited) // 看是否还需要增加updateTweet(Tweet tweet)方法 // 对所有相关表的对应消息都进行刷新(如果存在的话) // getDb().updateTweet(TwitterDbAdapter.TABLE_FAVORITE, tweet); // getDb().updateTweet(TwitterDbAdapter.TABLE_MENTION, tweet); // getDb().updateTweet(TwitterDbAdapter.TABLE_TWEET, tweet); } @Override protected boolean _onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate."); if (super._onCreate(savedInstanceState)) { // Mark all as read. // getDb().markAllMentionsRead(); markAllRead(); boolean shouldRetrieve = false; // FIXME: 该子类页面全部使用了这个统一的计时器,导致进入Mention等分页面后经常不会自动刷新 long lastRefreshTime = mPreferences.getLong( Preferences.LAST_TWEET_REFRESH_KEY, 0); long nowTime = DateTimeHelper.getNowTime(); long diff = nowTime - lastRefreshTime; Log.d(TAG, "Last refresh was " + diff + " ms ago."); if (diff > REFRESH_THRESHOLD) { shouldRetrieve = true; } else if (isTrue(savedInstanceState, SIS_RUNNING_KEY)) { // Check to see if it was running a send or retrieve task. // It makes no sense to resend the send request (don't want // dupes) // so we instead retrieve (refresh) to see if the message has // posted. Log.d(TAG, "Was last running a retrieve or send task. Let's refresh."); shouldRetrieve = true; } if (shouldRetrieve) { doRetrieve(); } goTop(); // skip the header long lastFollowersRefreshTime = mPreferences.getLong( Preferences.LAST_FOLLOWERS_REFRESH_KEY, 0); diff = nowTime - lastFollowersRefreshTime; Log.d(TAG, "Last followers refresh was " + diff + " ms ago."); // FIXME: 目前还没有对Followers列表做逻辑处理,因此暂时去除对Followers的获取。 // 未来需要实现@用户提示时,对Follower操作需要做一次review和refactoring // 现在频繁会出现主键冲突的问题。 // // Should Refresh Followers // if (diff > FOLLOWERS_REFRESH_THRESHOLD // && (mRetrieveTask == null || mRetrieveTask.getStatus() != // GenericTask.Status.RUNNING)) { // Log.d(TAG, "Refresh followers."); // doRetrieveFollowers(); // } // 手势识别 registerGestureListener(); //晃动刷新 registerShakeListener(); return true; } else { return false; } } @Override protected void onResume() { Log.d(TAG, "onResume."); if (lastPosition != 0) { mTweetList.setSelection(lastPosition); } if (mShaker != null){ mShaker.resume(); } super.onResume(); checkIsLogedIn(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mRetrieveTask != null && mRetrieveTask.getStatus() == GenericTask.Status.RUNNING) { outState.putBoolean(SIS_RUNNING_KEY, true); } } @Override protected void onRestoreInstanceState(Bundle bundle) { super.onRestoreInstanceState(bundle); // mTweetEdit.updateCharsRemain(); } @Override protected void onDestroy() { Log.d(TAG, "onDestroy."); super.onDestroy(); taskManager.cancelAll(); // 刷新按钮停止旋转 if (loadMoreGIF != null){ loadMoreGIF.setVisibility(View.GONE); } if (mTweetList != null){ mTweetList.onRefreshComplete(); } } @Override protected void onPause() { Log.d(TAG, "onPause."); if (mShaker != null){ mShaker.pause(); } super.onPause(); lastPosition = mTweetList.getFirstVisiblePosition(); } @Override protected void onRestart() { Log.d(TAG, "onRestart."); super.onRestart(); } @Override protected void onStart() { Log.d(TAG, "onStart."); super.onStart(); } @Override protected void onStop() { Log.d(TAG, "onStop."); super.onStop(); } // UI helpers. @Override protected String getActivityTitle() { return null; } @Override protected void adapterRefresh() { mTweetAdapter.notifyDataSetChanged(); mTweetAdapter.refresh(); } // Retrieve interface public void updateProgress(String progress) { mProgressText.setText(progress); } public void draw() { mTweetAdapter.refresh(); } public void goTop() { Log.d(TAG, "goTop."); mTweetList.setSelection(1); } private void doRetrieveFollowers() { Log.d(TAG, "Attempting followers retrieve."); if (mFollowersRetrieveTask != null && mFollowersRetrieveTask.getStatus() == GenericTask.Status.RUNNING) { return; } else { mFollowersRetrieveTask = new FollowersRetrieveTask(); mFollowersRetrieveTask.setListener(mFollowerRetrieveTaskListener); mFollowersRetrieveTask.execute(); taskManager.addTask(mFollowersRetrieveTask); // Don't need to cancel FollowersTask (assuming it ends properly). mFollowersRetrieveTask.setCancelable(false); } } public void doRetrieve() { Log.d(TAG, "Attempting retrieve."); if (mRetrieveTask != null && mRetrieveTask.getStatus() == GenericTask.Status.RUNNING) { return; } else { mRetrieveTask = new RetrieveTask(); mRetrieveTask.setFeedback(mFeedback); mRetrieveTask.setListener(mRetrieveTaskListener); mRetrieveTask.execute(); // Add Task to manager taskManager.addTask(mRetrieveTask); } } private class RetrieveTask extends GenericTask { private String _errorMsg; public String getErrorMsg() { return _errorMsg; } @Override protected TaskResult _doInBackground(TaskParams... params) { List<com.ch_linghu.fanfoudroid.fanfou.Status> statusList; try { String maxId = fetchMaxId(); // getDb().fetchMaxMentionId(); statusList = getMessageSinceId(maxId); } catch (HttpException e) { Log.e(TAG, e.getMessage(), e); _errorMsg = e.getMessage(); return TaskResult.IO_ERROR; } ArrayList<Tweet> tweets = new ArrayList<Tweet>(); for (com.ch_linghu.fanfoudroid.fanfou.Status status : statusList) { if (isCancelled()) { return TaskResult.CANCELLED; } tweets.add(Tweet.create(status)); if (isCancelled()) { return TaskResult.CANCELLED; } } publishProgress(SimpleFeedback.calProgressBySize(40, 20, tweets)); mRetrieveCount = addMessages(tweets, false); return TaskResult.OK; } } private class FollowersRetrieveTask extends GenericTask { @Override protected TaskResult _doInBackground(TaskParams... params) { try { // TODO: 目前仅做新API兼容性改动,待完善Follower处理 IDs followers = getApi().getFollowersIDs(); List<String> followerIds = Arrays.asList(followers.getIDs()); getDb().syncFollowers(followerIds); } catch (HttpException e) { Log.e(TAG, e.getMessage(), e); return TaskResult.IO_ERROR; } return TaskResult.OK; } } // GET MORE TASK private class GetMoreTask extends GenericTask { private String _errorMsg; public String getErrorMsg() { return _errorMsg; } @Override protected TaskResult _doInBackground(TaskParams... params) { List<com.ch_linghu.fanfoudroid.fanfou.Status> statusList; String minId = fetchMinId(); // getDb().fetchMaxMentionId(); if (minId == null) { return TaskResult.FAILED; } try { statusList = getMoreMessageFromId(minId); } catch (HttpException e) { Log.e(TAG, e.getMessage(), e); _errorMsg = e.getMessage(); return TaskResult.IO_ERROR; } if (statusList == null) { return TaskResult.FAILED; } ArrayList<Tweet> tweets = new ArrayList<Tweet>(); publishProgress(SimpleFeedback.calProgressBySize(40, 20, tweets)); for (com.ch_linghu.fanfoudroid.fanfou.Status status : statusList) { if (isCancelled()) { return TaskResult.CANCELLED; } tweets.add(Tweet.create(status)); if (isCancelled()) { return TaskResult.CANCELLED; } } addMessages(tweets, false); // getDb().addMentions(tweets, false); return TaskResult.OK; } } public void doGetMore() { Log.d(TAG, "Attempting getMore."); if (mGetMoreTask != null && mGetMoreTask.getStatus() == GenericTask.Status.RUNNING) { return; } else { mGetMoreTask = new GetMoreTask(); mGetMoreTask.setFeedback(mFeedback); mGetMoreTask.setListener(mRetrieveTaskListener); mGetMoreTask.execute(); // Add Task to manager taskManager.addTask(mGetMoreTask); } } //////////////////// Gesture test ///////////////////////////////////// private static boolean useGestrue; { useGestrue = TwitterApplication.mPref.getBoolean( Preferences.USE_GESTRUE, false); if (useGestrue) { Log.v(TAG, "Using Gestrue!"); } else { Log.v(TAG, "Not Using Gestrue!"); } } //////////////////// Gesture test ///////////////////////////////////// private static boolean useShake; { useShake = TwitterApplication.mPref.getBoolean( Preferences.USE_SHAKE, false); if (useShake) { Log.v(TAG, "Using Shake to refresh!"); } else { Log.v(TAG, "Not Using Shake!"); } } protected FlingGestureListener myGestureListener = null; @Override public boolean onTouchEvent(MotionEvent event) { if (useGestrue && myGestureListener != null) { return myGestureListener.getDetector().onTouchEvent(event); } return super.onTouchEvent(event); } // use it in _onCreate private void registerGestureListener() { if (useGestrue) { myGestureListener = new FlingGestureListener(this, MyActivityFlipper.create(this)); getTweetList().setOnTouchListener(myGestureListener); } } // use it in _onCreate private void registerShakeListener() { if (useShake){ mShaker = new ShakeListener(this); mShaker.setOnShakeListener(new ShakeListener.OnShakeListener() { @Override public void onShake() { Log.v(TAG, "onShake"); doRetrieve(); } }); } } }