Java tutorial
/* * Copyright (C) 2008 Torgny Bjers * * 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.xorcode.andtweet; import org.json.JSONObject; import android.app.SearchManager; import android.content.ContentUris; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.provider.SearchRecentSuggestions; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.LinearLayout; import android.widget.SimpleCursorAdapter; import android.widget.Toast; import com.xorcode.andtweet.AndTweetService.CommandData; import com.xorcode.andtweet.AndTweetService.CommandEnum; import com.xorcode.andtweet.TwitterUser.CredentialsVerified; import com.xorcode.andtweet.data.AndTweetDatabase; import com.xorcode.andtweet.data.MyPreferences; import com.xorcode.andtweet.data.PagedCursorAdapter; import com.xorcode.andtweet.data.TimelineSearchSuggestionProvider; import com.xorcode.andtweet.data.TweetBinder; import com.xorcode.andtweet.data.AndTweetDatabase.Tweets; import com.xorcode.andtweet.util.MyLog; import com.xorcode.andtweet.util.SelectionAndArgs; /** * @author torgny.bjers */ public class TweetListActivity extends TimelineActivity { private static final String TAG = TweetListActivity.class.getSimpleName(); // Context menu items public static final int CONTEXT_MENU_ITEM_REPLY = Menu.FIRST + 2; public static final int CONTEXT_MENU_ITEM_FAVORITE = Menu.FIRST + 3; public static final int CONTEXT_MENU_ITEM_DIRECT_MESSAGE = Menu.FIRST + 4; public static final int CONTEXT_MENU_ITEM_UNFOLLOW = Menu.FIRST + 5; public static final int CONTEXT_MENU_ITEM_BLOCK = Menu.FIRST + 6; public static final int CONTEXT_MENU_ITEM_RETWEET = Menu.FIRST + 7; public static final int CONTEXT_MENU_ITEM_PROFILE = Menu.FIRST + 8; public static final int CONTEXT_MENU_ITEM_DESTROY_FAVORITE = Menu.FIRST + 9; public static final int CONTEXT_MENU_ITEM_DESTROY_STATUS = Menu.FIRST + 10; public static final int CONTEXT_MENU_ITEM_SHARE = Menu.FIRST + 11; private boolean mInitializing = false; // Table columns to use for the tweets data private static final String[] PROJECTION = new String[] { Tweets._ID, Tweets.AUTHOR_ID, Tweets.MESSAGE, Tweets.IN_REPLY_TO_AUTHOR_ID, Tweets.FAVORITED, Tweets.SENT_DATE }; /** * Called when the activity is first created. * * @see android.app.Activity#onCreate(android.os.Bundle) * @param savedInstanceState */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onCreate"); } // Create list footer for loading messages mListFooter = new LinearLayout(getApplicationContext()); mListFooter.setClickable(false); getListView().addFooterView(mListFooter); LayoutInflater inflater = LayoutInflater.from(getApplicationContext()); View tv = inflater.inflate(R.layout.item_loading, null); mListFooter.addView(tv, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); mListFooter.setVisibility(View.INVISIBLE); getListView().setOnScrollListener(this); initUI(); } @Override public boolean onSearchRequested() { Bundle appDataBundle = new Bundle(); appDataBundle.putParcelable("content_uri", AndTweetDatabase.Tweets.SEARCH_URI); startSearch(null, false, appDataBundle, false); return true; } @Override public void onNewIntent(Intent newIntent) { super.onNewIntent(newIntent); // All actions are actually search actions... // So get and process search query here queryListData(newIntent, false, false); } /** * Prepare query to the ContentProvider (to the database) and load List of Tweets with data * @param queryIntent * @param otherThread This method is being accessed from other thread * @param loadOneMorePage load one more page of tweets */ protected void queryListData(Intent queryIntent, boolean otherThread, boolean loadOneMorePage) { // The search query is provided as an "extra" string in the query intent // TODO maybe use mQueryString here... String queryString = queryIntent.getStringExtra(SearchManager.QUERY); Intent intent = getIntent(); if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "doSearchQuery; queryString=\"" + queryString + "\"; TimelineType=" + mTimelineType); } Uri contentUri = Tweets.CONTENT_URI; SelectionAndArgs sa = new SelectionAndArgs(); String sortOrder = Tweets.DEFAULT_SORT_ORDER; // Id of the last (oldest) tweet to retrieve long lastItemId = -1; if (queryString != null && queryString.length() > 0) { // Record the query string in the recent queries suggestions // provider SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, TimelineSearchSuggestionProvider.AUTHORITY, TimelineSearchSuggestionProvider.MODE); suggestions.saveRecentQuery(queryString, null); contentUri = Tweets.SEARCH_URI; contentUri = Uri.withAppendedPath(contentUri, Uri.encode(queryString)); } intent.putExtra(SearchManager.QUERY, queryString); if (!contentUri.equals(intent.getData())) { intent.setData(contentUri); } if (sa.nArgs == 0) { // In fact this is needed every time you want to load next page of // tweets. // So we have to duplicate here everything we set in // com.xorcode.andtweet.TimelineActivity.onOptionsItemSelected() sa.clear(); sa.addSelection(Tweets.TWEET_TYPE + " IN (?, ?)", new String[] { String.valueOf(Tweets.TIMELINE_TYPE_FRIENDS), String.valueOf(Tweets.TIMELINE_TYPE_MENTIONS) }); if (mTimelineType == Tweets.TIMELINE_TYPE_FAVORITES) { sa.addSelection(AndTweetDatabase.Tweets.FAVORITED + " = ?", new String[] { "1" }); } if (mTimelineType == Tweets.TIMELINE_TYPE_MENTIONS) { sa.addSelection(Tweets.MESSAGE + " LIKE ?", new String[] { "%@" + TwitterUser.getTwitterUser().getUsername() + "%" }); } } if (!positionRestored) { // We have to ensure that saved position will be // loaded from database into the list lastItemId = getSavedPosition(true); } int nTweets = 0; if (mCursor != null && !mCursor.isClosed()) { if (positionRestored) { // If position is NOT loaded - this cursor is from other // timeline/search // and we shouldn't care how much rows are there. nTweets = mCursor.getCount(); } if (!otherThread) { mCursor.close(); } } if (lastItemId > 0) { if (sa.nArgs == 0) { sa.addSelection("AndTweetDatabase.Tweets.TWEET_TYPE" + " IN (?, ?)" + ")", new String[] { String.valueOf(Tweets.TIMELINE_TYPE_FRIENDS), String.valueOf(Tweets.TIMELINE_TYPE_MENTIONS) }); } sa.addSelection(Tweets._ID + " >= ?", new String[] { String.valueOf(lastItemId) }); } else { if (loadOneMorePage) { nTweets += PAGE_SIZE; } else if (nTweets < PAGE_SIZE) { nTweets = PAGE_SIZE; } sortOrder += " LIMIT 0," + nTweets; } // This is for testing pruneOldRecords // try { // FriendTimeline fl = new FriendTimeline(TweetListActivity.this, // AndTweetDatabase.Tweets.TIMELINE_TYPE_FRIENDS); // fl.pruneOldRecords(); // // } catch (Exception e) { // e.printStackTrace(); // } mCursor = getContentResolver().query(contentUri, PROJECTION, sa.selection, sa.selectionArgs, sortOrder); if (!otherThread) { createAdapters(); } } /** * TODO: Maybe this code should be moved to "onResume" ??? */ @Override protected void onStart() { super.onStart(); if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onStart"); } Intent intent = getIntent(); queryListData(intent, false, false); if (mTweetEditor.isVisible()) { // This is done to request focus (if we need this...) mTweetEditor.show(); } } @Override protected void onResume() { super.onResume(); if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onResume"); } if (TwitterUser.getTwitterUser().getCredentialsVerified() == CredentialsVerified.SUCCEEDED) { if (!TwitterUser.getTwitterUser().getSharedPreferences().getBoolean("loadedOnce", false)) { TwitterUser.getTwitterUser().getSharedPreferences().edit().putBoolean("loadedOnce", true).commit(); // One-time "manually" load tweets from the Internet for the new TwitterUser manualReload(); } } } @Override protected void onStop() { super.onStop(); if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } //disconnectService(); } @Override protected void onSaveInstanceState(Bundle outState) { outState.putBoolean(BUNDLE_KEY_IS_LOADING, mIsLoading); super.onSaveInstanceState(outState); } @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, view, menuInfo); // Get the adapter context menu information AdapterView.AdapterContextMenuInfo info; try { info = (AdapterView.AdapterContextMenuInfo) menuInfo; } catch (ClassCastException e) { Log.e(TAG, "bad menuInfo", e); return; } int m = 0; // Add menu items menu.add(0, CONTEXT_MENU_ITEM_REPLY, m++, R.string.menu_item_reply); menu.add(0, CONTEXT_MENU_ITEM_RETWEET, m++, R.string.menu_item_retweet); menu.add(0, CONTEXT_MENU_ITEM_SHARE, m++, R.string.menu_item_share); // menu.add(0, CONTEXT_MENU_ITEM_DIRECT_MESSAGE, m++, // R.string.menu_item_direct_message); // menu.add(0, CONTEXT_MENU_ITEM_UNFOLLOW, m++, // R.string.menu_item_unfollow); // menu.add(0, CONTEXT_MENU_ITEM_BLOCK, m++, R.string.menu_item_block); // menu.add(0, CONTEXT_MENU_ITEM_PROFILE, m++, // R.string.menu_item_view_profile); // Get the record for the currently selected item Uri uri = ContentUris.withAppendedId(Tweets.CONTENT_URI, info.id); Cursor c = getContentResolver().query(uri, new String[] { Tweets._ID, Tweets.MESSAGE, Tweets.AUTHOR_ID, Tweets.FAVORITED }, null, null, null); try { c.moveToFirst(); menu.setHeaderTitle(c.getString(c.getColumnIndex(Tweets.MESSAGE))); if (c.getInt(c.getColumnIndex(Tweets.FAVORITED)) == 1) { menu.add(0, CONTEXT_MENU_ITEM_DESTROY_FAVORITE, m++, R.string.menu_item_destroy_favorite); } else { menu.add(0, CONTEXT_MENU_ITEM_FAVORITE, m++, R.string.menu_item_favorite); } if (MyPreferences.getDefaultSharedPreferences().getString(MyPreferences.KEY_TWITTER_USERNAME, null) .equals(c.getString(c.getColumnIndex(Tweets.AUTHOR_ID)))) { menu.add(0, CONTEXT_MENU_ITEM_DESTROY_STATUS, m++, R.string.menu_item_destroy_status); } } catch (Exception e) { Log.e(TAG, "onCreateContextMenu: " + e.toString()); } finally { if (c != null && !c.isClosed()) c.close(); } } @Override public boolean onContextItemSelected(MenuItem item) { super.onContextItemSelected(item); AdapterView.AdapterContextMenuInfo info; try { info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); } catch (ClassCastException e) { Log.e(TAG, "bad menuInfo", e); return false; } mCurrentId = info.id; Uri uri; Cursor c; switch (item.getItemId()) { case CONTEXT_MENU_ITEM_REPLY: uri = ContentUris.withAppendedId(Tweets.CONTENT_URI, info.id); c = getContentResolver().query(uri, new String[] { Tweets._ID, Tweets.AUTHOR_ID }, null, null, null); try { c.moveToFirst(); String reply = "@" + c.getString(c.getColumnIndex(Tweets.AUTHOR_ID)) + " "; long replyId = c.getLong(c.getColumnIndex(Tweets._ID)); mTweetEditor.startEditing(reply, replyId); } catch (Exception e) { Log.e(TAG, "onContextItemSelected: " + e.toString()); return false; } finally { if (c != null && !c.isClosed()) c.close(); } return true; case CONTEXT_MENU_ITEM_RETWEET: uri = ContentUris.withAppendedId(Tweets.CONTENT_URI, info.id); c = getContentResolver().query(uri, new String[] { Tweets._ID, Tweets.AUTHOR_ID, Tweets.MESSAGE }, null, null, null); try { c.moveToFirst(); StringBuilder message = new StringBuilder(); String reply = "RT @" + c.getString(c.getColumnIndex(Tweets.AUTHOR_ID)) + " "; message.append(reply); CharSequence text = c.getString(c.getColumnIndex(Tweets.MESSAGE)); int len = 140 - reply.length() - 3; if (text.length() < len) { len = text.length(); } message.append(text, 0, len); if (message.length() == 137) { message.append("..."); } mTweetEditor.startEditing(message.toString(), 0); } catch (Exception e) { Log.e(TAG, "onContextItemSelected: " + e.toString()); return false; } finally { if (c != null && !c.isClosed()) c.close(); } return true; case CONTEXT_MENU_ITEM_DESTROY_STATUS: sendCommand(new CommandData(CommandEnum.DESTROY_STATUS, mCurrentId)); return true; case CONTEXT_MENU_ITEM_FAVORITE: sendCommand(new CommandData(CommandEnum.CREATE_FAVORITE, mCurrentId)); return true; case CONTEXT_MENU_ITEM_DESTROY_FAVORITE: sendCommand(new CommandData(CommandEnum.DESTROY_FAVORITE, mCurrentId)); return true; case CONTEXT_MENU_ITEM_SHARE: uri = ContentUris.withAppendedId(Tweets.CONTENT_URI, info.id); c = getContentResolver().query(uri, new String[] { Tweets._ID, Tweets.AUTHOR_ID, Tweets.MESSAGE }, null, null, null); try { c.moveToFirst(); StringBuilder subject = new StringBuilder(); StringBuilder text = new StringBuilder(); String message = c.getString(c.getColumnIndex(Tweets.MESSAGE)); subject.append(getText(R.string.button_create_tweet)); subject.append(" - " + message); int maxlength = 80; if (subject.length() > maxlength) { subject.setLength(maxlength); // Truncate at the last space subject.setLength(subject.lastIndexOf(" ")); subject.append("..."); } text.append(message); text.append("\n-- \n" + c.getString(c.getColumnIndex(Tweets.AUTHOR_ID))); text.append("\n URL: " + "http://twitter.com/" + c.getString(c.getColumnIndex(Tweets.AUTHOR_ID)) + "/status/" + c.getString(c.getColumnIndex(Tweets._ID))); Intent share = new Intent(android.content.Intent.ACTION_SEND); share.setType("text/plain"); share.putExtra(Intent.EXTRA_SUBJECT, subject.toString()); share.putExtra(Intent.EXTRA_TEXT, text.toString()); startActivity(Intent.createChooser(share, getText(R.string.menu_item_share))); } catch (Exception e) { Log.e(TAG, "onContextItemSelected: " + e.toString()); return false; } finally { if (c != null && !c.isClosed()) c.close(); } return true; case CONTEXT_MENU_ITEM_UNFOLLOW: case CONTEXT_MENU_ITEM_BLOCK: case CONTEXT_MENU_ITEM_DIRECT_MESSAGE: case CONTEXT_MENU_ITEM_PROFILE: Toast.makeText(this, R.string.unimplemented, Toast.LENGTH_SHORT).show(); return true; } return false; } /** * Create adapters */ private void createAdapters() { int listItemId = R.layout.tweetlist_item; if (MyPreferences.getDefaultSharedPreferences().getBoolean("appearance_use_avatars", false)) { listItemId = R.layout.tweetlist_item_avatar; } PagedCursorAdapter tweetsAdapter = new PagedCursorAdapter(TweetListActivity.this, listItemId, mCursor, new String[] { Tweets.AUTHOR_ID, Tweets.MESSAGE, Tweets.SENT_DATE, Tweets.FAVORITED }, new int[] { R.id.tweet_screen_name, R.id.tweet_message, R.id.tweet_sent, R.id.tweet_favorite }, getIntent().getData(), PROJECTION, Tweets.DEFAULT_SORT_ORDER); tweetsAdapter.setViewBinder(new TweetBinder()); setListAdapter(tweetsAdapter); } /** * Listener that checks for clicks on the main list view. * * @param adapterView * @param view * @param position * @param id */ @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onItemClick, id=" + id); } if (id <= 0) { return; } Uri uri = ContentUris.withAppendedId(Tweets.CONTENT_URI, id); String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) { if (MyLog.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onItemClick, setData=" + uri); } setResult(RESULT_OK, new Intent().setData(uri)); } else { if (MyLog.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onItemClick, startActivity=" + uri); } startActivity(new Intent(Intent.ACTION_VIEW, uri)); } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { mTotalItemCount = totalItemCount; if (positionRestored && !mIsLoading) { // Idea from // http://stackoverflow.com/questions/1080811/android-endless-list boolean loadMore = (visibleItemCount > 0) && (firstVisibleItem > 0) && (firstVisibleItem + visibleItemCount >= totalItemCount); if (loadMore) { mIsLoading = true; MyLog.d(TAG, "Start Loading more items, total=" + totalItemCount); // setProgressBarIndeterminateVisibility(true); mListFooter.setVisibility(View.VISIBLE); Thread thread = new Thread(mLoadListItems); thread.start(); } } } /** * Updates the activity title. */ @Override public void updateTitle() { // First set less detailed title super.updateTitle(); // Then start asynchronous task that will set detailed info sendCommand(new CommandData(CommandEnum.RATE_LIMIT_STATUS)); } { /** * Message handler for messages from threads. */ mHandler = new Handler() { /** * Message handler * * @param msg */ @Override public void handleMessage(Message msg) { JSONObject result = null; switch (msg.what) { case MSG_TWEETS_CHANGED: int numTweets = msg.arg1; if (numTweets > 0) { mNM.cancelAll(); } break; case MSG_DATA_LOADING: mIsLoading = (msg.arg1 == 1) ? true : false; if (!mIsLoading) { Toast.makeText(TweetListActivity.this, R.string.timeline_reloaded, Toast.LENGTH_SHORT) .show(); } break; case MSG_UPDATE_STATUS: result = (JSONObject) msg.obj; if (result == null) { Toast.makeText(TweetListActivity.this, R.string.error_connection_error, Toast.LENGTH_LONG) .show(); } else if (result.optString("error").length() > 0) { Toast.makeText(TweetListActivity.this, (CharSequence) result.optString("error"), Toast.LENGTH_LONG).show(); } else { // The tweet was sent successfully Toast.makeText(TweetListActivity.this, R.string.message_sent, Toast.LENGTH_SHORT).show(); } break; case MSG_AUTHENTICATION_ERROR: mListFooter.setVisibility(View.INVISIBLE); showDialog(DIALOG_AUTHENTICATION_FAILED); break; case MSG_SERVICE_UNAVAILABLE_ERROR: mListFooter.setVisibility(View.INVISIBLE); showDialog(DIALOG_SERVICE_UNAVAILABLE); break; case MSG_MANUAL_RELOAD: mIsLoading = false; Toast.makeText(TweetListActivity.this, R.string.timeline_reloaded, Toast.LENGTH_SHORT).show(); mListFooter.setVisibility(View.INVISIBLE); updateTitle(); if (mInitializing) { mInitializing = false; bindToService(); } break; case MSG_LOAD_ITEMS: mListFooter.setVisibility(View.INVISIBLE); switch (msg.arg1) { case STATUS_LOAD_ITEMS_SUCCESS: updateTitle(); mListFooter.setVisibility(View.INVISIBLE); if (positionRestored) { // This will prevent continuous loading... if (mCursor.getCount() > getListAdapter().getCount()) { ((SimpleCursorAdapter) getListAdapter()).changeCursor(mCursor); } } mIsLoading = false; // setProgressBarIndeterminateVisibility(false); break; case STATUS_LOAD_ITEMS_FAILURE: break; } break; case MSG_UPDATED_TITLE: if (msg.arg1 > 0) { TweetListActivity.super.updateTitle(msg.arg1 + "/" + msg.arg2); } break; case MSG_CONNECTION_TIMEOUT_EXCEPTION: mListFooter.setVisibility(View.INVISIBLE); showDialog(DIALOG_CONNECTION_TIMEOUT); break; case MSG_STATUS_DESTROY: result = (JSONObject) msg.obj; if (result == null) { Toast.makeText(TweetListActivity.this, R.string.error_connection_error, Toast.LENGTH_LONG) .show(); } else if (result.optString("error").length() > 0) { Toast.makeText(TweetListActivity.this, (CharSequence) result.optString("error"), Toast.LENGTH_LONG).show(); } else { Toast.makeText(TweetListActivity.this, R.string.status_destroyed, Toast.LENGTH_SHORT) .show(); mCurrentId = 0; } break; case MSG_FAVORITE_CREATE: result = (JSONObject) msg.obj; if (result == null) { Toast.makeText(TweetListActivity.this, R.string.error_connection_error, Toast.LENGTH_LONG) .show(); } else if (result.optString("error").length() > 0) { Toast.makeText(TweetListActivity.this, (CharSequence) result.optString("error"), Toast.LENGTH_LONG).show(); } else { Toast.makeText(TweetListActivity.this, R.string.favorite_created, Toast.LENGTH_SHORT) .show(); mCurrentId = 0; } break; case MSG_FAVORITE_DESTROY: result = (JSONObject) msg.obj; if (result == null) { Toast.makeText(TweetListActivity.this, R.string.error_connection_error, Toast.LENGTH_LONG) .show(); } else if (result.optString("error").length() > 0) { Toast.makeText(TweetListActivity.this, (CharSequence) result.optString("error"), Toast.LENGTH_LONG).show(); } else { Toast.makeText(TweetListActivity.this, R.string.favorite_destroyed, Toast.LENGTH_SHORT) .show(); mCurrentId = 0; } break; case MSG_CONNECTION_EXCEPTION: switch (msg.arg1) { case MSG_FAVORITE_CREATE: case MSG_FAVORITE_DESTROY: case MSG_STATUS_DESTROY: try { dismissDialog(DIALOG_EXECUTING_COMMAND); } catch (IllegalArgumentException e) { MyLog.d(TAG, "", e); } break; } Toast.makeText(TweetListActivity.this, R.string.error_connection_error, Toast.LENGTH_SHORT) .show(); break; default: super.handleMessage(msg); } } }; } /** * Load more items from the database into the list. This procedure doesn't * download any new tweets from the Internet */ protected Runnable mLoadListItems = new Runnable() { public void run() { if (MyLog.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "mLoadListItems run"); } queryListData(TweetListActivity.this.getIntent(), true, true); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_LOAD_ITEMS, STATUS_LOAD_ITEMS_SUCCESS, 0), 400); } }; }