MessageList.java revision 7c3cca80a0b4dacb7bcb48c65a9999138b5df82b
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.activity; 18 19import com.android.email.Controller; 20import com.android.email.R; 21import com.android.email.Utility; 22import com.android.email.activity.setup.AccountSettings; 23import com.android.email.mail.MessagingException; 24import com.android.email.provider.EmailContent; 25import com.android.email.provider.EmailContent.MessageColumns; 26 27import android.app.ListActivity; 28import android.content.ContentUris; 29import android.content.ContentValues; 30import android.content.Context; 31import android.content.Intent; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.graphics.drawable.Drawable; 35import android.net.Uri; 36import android.os.AsyncTask; 37import android.os.Bundle; 38import android.os.Handler; 39import android.view.ContextMenu; 40import android.view.LayoutInflater; 41import android.view.Menu; 42import android.view.MenuItem; 43import android.view.View; 44import android.view.ViewGroup; 45import android.view.Window; 46import android.view.ContextMenu.ContextMenuInfo; 47import android.view.View.OnClickListener; 48import android.widget.AdapterView; 49import android.widget.CursorAdapter; 50import android.widget.ImageView; 51import android.widget.ListView; 52import android.widget.TextView; 53import android.widget.Toast; 54import android.widget.AdapterView.OnItemClickListener; 55 56import java.util.Date; 57import java.util.HashSet; 58 59public class MessageList extends ListActivity implements OnItemClickListener, OnClickListener { 60 61 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 62 private static final String EXTRA_ACCOUNT_NAME = "com.android.email.activity.ACCOUNT_NAME"; 63 private static final String EXTRA_MAILBOX_NAME = "com.android.email.activity.MAILBOX_NAME"; 64 65 // UI support 66 private ListView mListView; 67 private MessageListAdapter mListAdapter; 68 private MessageListHandler mHandler = new MessageListHandler(); 69 private ControllerResults mControllerCallback = new ControllerResults(); 70 71 // DB access 72 private long mMailboxId; 73 private LoadMessagesTask mLoadMessagesTask; 74 75 /** 76 * Open a specific mailbox. 77 * 78 * TODO This should just shortcut to a more generic version that can accept a list of 79 * accounts/mailboxes (e.g. merged inboxes). 80 * 81 * @param context 82 * @param id mailbox key 83 * @param accountName the account we're viewing 84 * @param mailboxName the mailbox we're viewing 85 */ 86 public static void actionHandleAccount(Context context, long id, 87 String accountName, String mailboxName) { 88 Intent intent = new Intent(context, MessageList.class); 89 intent.putExtra(EXTRA_MAILBOX_ID, id); 90 intent.putExtra(EXTRA_ACCOUNT_NAME, accountName); 91 intent.putExtra(EXTRA_MAILBOX_NAME, mailboxName); 92 context.startActivity(intent); 93 } 94 95 @Override 96 public void onCreate(Bundle icicle) { 97 super.onCreate(icicle); 98 99 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 100 101 setContentView(R.layout.message_list); 102 mListView = getListView(); 103 mListView.setOnItemClickListener(this); 104 mListView.setItemsCanFocus(false); 105 registerForContextMenu(mListView); 106 107 mListAdapter = new MessageListAdapter(this); 108 setListAdapter(mListAdapter); 109 mListView.setAdapter(mAdapter); 110 111 // TODO set title to "account > mailbox (#unread)" 112 113 // TODO extend this to properly deal with multiple mailboxes, cursor, etc. 114 mMailboxId = getIntent().getLongExtra(EXTRA_MAILBOX_ID, -1); 115 116 mLoadMessagesTask = (LoadMessagesTask) new LoadMessagesTask(mMailboxId).execute(); 117 } 118 119 @Override 120 public void onPause() { 121 super.onPause(); 122 Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); 123 } 124 125 @Override 126 public void onResume() { 127 super.onResume(); 128 Controller.getInstance(getApplication()).addResultCallback(mControllerCallback); 129 130 // TODO: may need to clear notifications here 131 } 132 133 @Override 134 protected void onDestroy() { 135 super.onDestroy(); 136 137 if (mLoadMessagesTask != null && 138 mLoadMessagesTask.getStatus() != LoadMessagesTask.Status.FINISHED) { 139 mLoadMessagesTask.cancel(true); 140 mLoadMessagesTask = null; 141 } 142 } 143 144 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 145 onOpenMessage(id); 146 } 147 148 public void onClick(View v) { 149 // TODO Auto-generated method stub 150 151 } 152 153 @Override 154 public boolean onCreateOptionsMenu(Menu menu) { 155 super.onCreateOptionsMenu(menu); 156 getMenuInflater().inflate(R.menu.message_list_option, menu); 157 return true; 158 } 159 160 @Override 161 public boolean onOptionsItemSelected(MenuItem item) { 162 switch (item.getItemId()) { 163 case R.id.refresh: 164 onRefresh(); 165 return true; 166 case R.id.accounts: 167 onAccounts(); 168 return true; 169 case R.id.compose: 170 onCompose(); 171 return true; 172 case R.id.account_settings: 173 onEditAccount(); 174 return true; 175 default: 176 return super.onOptionsItemSelected(item); 177 } 178 } 179 180 @Override 181 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 182 super.onCreateContextMenu(menu, v, menuInfo); 183 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 184 185 // TODO: There is no context menu for the outbox 186 // TODO: There is probably a special context menu for the trash 187 188 getMenuInflater().inflate(R.menu.message_list_context, menu); 189 190 // TODO: flip the "mark as read" string if the message is already read 191 // In order to do this, I really should cache the read state in the item view, 192 // instead of re-reading data from the cursor. 193 } 194 195 @Override 196 public boolean onContextItemSelected(MenuItem item) { 197 AdapterView.AdapterContextMenuInfo info = 198 (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 199 200 switch (item.getItemId()) { 201 case R.id.open: 202 onOpenMessage(info.id); 203 break; 204 case R.id.delete: 205 onDelete(info.id); 206 break; 207 case R.id.reply: 208 //onReply(holder); 209 break; 210 case R.id.reply_all: 211 //onReplyAll(holder); 212 break; 213 case R.id.forward: 214 //onForward(holder); 215 break; 216 case R.id.mark_as_read: 217 onToggleRead(info.id, info.targetView); 218 break; 219 } 220 return super.onContextItemSelected(item); 221 } 222 223 private void onRefresh() { 224 // TODO: This needs to loop through all open mailboxes (there might be more than one) 225 EmailContent.Mailbox mailbox = 226 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 227 EmailContent.Account account = 228 EmailContent.Account.restoreAccountWithId(this, mailbox.mAccountKey); 229 mHandler.progress(true); 230 Controller.getInstance(getApplication()).updateMailbox( 231 account, mailbox, mControllerCallback); 232 } 233 234 private void onAccounts() { 235 Accounts.actionShowAccounts(this); 236 finish(); 237 } 238 239 private void onCompose() { 240 // TODO: Select correct account to send from when there are multiple mailboxes 241 EmailContent.Mailbox mailbox = 242 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 243 MessageCompose.actionCompose(this, mailbox.mAccountKey); 244 } 245 246 private void onEditAccount() { 247 // TODO: Select correct account to edit when there are multiple mailboxes 248 EmailContent.Mailbox mailbox = 249 EmailContent.Mailbox.restoreMailboxWithId(this, mMailboxId); 250 AccountSettings.actionSettings(this, mailbox.mAccountKey); 251 } 252 253 public void onOpenMessage(long messageId) { 254 // TODO Necessary info about the mailbox should have been cached in the listview item 255 // Instead, we're going to pull it from the DB here (expensively and in the wrong thread) 256 EmailContent.Message message = EmailContent.Message.restoreMessageWithId(this, messageId); 257 EmailContent.Mailbox mailbox = 258 EmailContent.Mailbox.restoreMailboxWithId(this, message.mMailboxKey); 259 260 if (mailbox.mType == EmailContent.Mailbox.TYPE_DRAFTS) { 261 // TODO need id-based API for MessageCompose 262 // MessageCompose.actionEditDraft(this, messageId); 263 } 264 else { 265 MessageView.actionView(this, messageId); 266 } 267 } 268 269 private void onDelete(long messageId) { 270 Controller.getInstance(getApplication()).deleteMessage(messageId, -1); 271 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 272 } 273 274 private void onToggleRead(long messageId, View itemView) { 275 // TODO the read-unread state of the given message should be cached in the listview item. 276 // Instead, we're going to pull it from the DB here (expensively and in the wrong thread) 277 EmailContent.Message message = EmailContent.Message.restoreMessageWithId(this, messageId); 278 boolean isRead = ! message.mFlagRead; 279 280 // TODO this should be a call to the controller, since it may possibly kick off 281 // more than just a DB update. Also, the DB update shouldn't be in the UI thread 282 // as it is here. 283 ContentValues cv = new ContentValues(); 284 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead ? 1 : 0); 285 Uri uri = ContentUris.withAppendedId( 286 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 287 getContentResolver().update(uri, cv, null, null); 288 } 289 290 /** 291 * Async task for loading a single folder out of the UI thread 292 * 293 * TODO: Extend API to support compound select (e.g. merged inbox list) 294 */ 295 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 296 297 private long mMailboxKey; 298 299 /** 300 * Special constructor to cache some local info 301 */ 302 public LoadMessagesTask(long mailboxKey) { 303 mMailboxKey = mailboxKey; 304 } 305 306 @Override 307 protected Cursor doInBackground(Void... params) { 308 return MessageList.this.managedQuery( 309 EmailContent.Message.CONTENT_URI, 310 MessageListAdapter.PROJECTION, 311 EmailContent.MessageColumns.MAILBOX_KEY + "=?", 312 new String[] { 313 String.valueOf(mMailboxKey) 314 }, 315 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 316 } 317 318 @Override 319 protected void onPostExecute(Cursor cursor) { 320 MessageList.this.mListAdapter.changeCursor(cursor); 321 322 // TODO: remove this hack and only update at the right time 323 if (cursor != null && cursor.getCount() == 0) { 324 onRefresh(); 325 } 326 } 327 } 328 329 /** 330 * Handler for UI-thread operations (when called from callbacks or any other threads) 331 */ 332 class MessageListHandler extends Handler { 333 private static final int MSG_PROGRESS = 1; 334 335 @Override 336 public void handleMessage(android.os.Message msg) { 337 switch (msg.what) { 338 case MSG_PROGRESS: 339 setProgressBarIndeterminateVisibility(msg.arg1 != 0); 340 break; 341 default: 342 super.handleMessage(msg); 343 } 344 } 345 346 public void progress(boolean progress) { 347 android.os.Message msg =android.os.Message.obtain(); 348 msg.what = MSG_PROGRESS; 349 msg.arg1 = progress ? 1 : 0; 350 sendMessage(msg); 351 } 352 } 353 354 /** 355 * Callback for async Controller results. This is all a placeholder until we figure out the 356 * final way to do this. 357 */ 358 private class ControllerResults implements Controller.Result { 359 public void updateMailboxListCallback(MessagingException result, long accountKey) { 360 } 361 362 public void updateMailboxCallback(MessagingException result, long accountKey, 363 long mailboxKey, int totalMessagesInMailbox, int numNewMessages) { 364 mHandler.progress(false); 365 } 366 } 367 368 /** 369 * This class implements the adapter for displaying messages based on cursors. 370 */ 371 private static class MessageListAdapter extends CursorAdapter { 372 373 public static final int COLUMN_ID = 0; 374 public static final int COLUMN_MAILBOX_KEY = 1; 375 public static final int COLUMN_DISPLAY_NAME = 2; 376 public static final int COLUMN_SUBJECT = 3; 377 public static final int COLUMN_DATE = 4; 378 public static final int COLUMN_READ = 5; 379 public static final int COLUMN_FAVORITE = 6; 380 public static final int COLUMN_ATTACHMENTS = 7; 381 382 public static final String[] PROJECTION = new String[] { 383 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, 384 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 385 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 386 }; 387 388 Context mContext; 389 private LayoutInflater mInflater; 390 private Drawable mAttachmentIcon; 391 private Drawable mFavoriteIconOn; 392 private Drawable mFavoriteIconOff; 393 private Drawable mSelectedIconOn; 394 private Drawable mSelectedIconOff; 395 396 private java.text.DateFormat mDateFormat; 397 private java.text.DateFormat mDayFormat; 398 private java.text.DateFormat mTimeFormat; 399 400 private HashSet<Long> mChecked = new HashSet<Long>(); 401 402 public MessageListAdapter(Context context) { 403 super(context, null); 404 mContext = context; 405 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 406 407 Resources resources = context.getResources(); 408 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 409 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 410 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 411 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 412 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 413 414 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 415 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 416 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 417 } 418 419 @Override 420 public void bindView(View view, Context context, Cursor cursor) { 421 View clipView = view.findViewById(R.id.chip); 422 boolean readFlag = cursor.getInt(COLUMN_READ) != 0; 423 clipView.getBackground().setAlpha(readFlag ? 0 : 255); 424 425 TextView fromView = (TextView) view.findViewById(R.id.from); 426 String text = cursor.getString(COLUMN_DISPLAY_NAME); 427 if (text != null) fromView.setText(text); 428 429 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 430 fromView.setCompoundDrawablesWithIntrinsicBounds(null, null, 431 hasAttachments ? mAttachmentIcon : null, null); 432 433 TextView subjectView = (TextView) view.findViewById(R.id.subject); 434 text = cursor.getString(COLUMN_SUBJECT); 435 if (text != null) subjectView.setText(text); 436 437 // TODO ui spec suggests "time", "day", "date" - implement "day" 438 TextView dateView = (TextView) view.findViewById(R.id.date); 439 long timestamp = cursor.getLong(COLUMN_DATE); 440 Date date = new Date(timestamp); 441 if (Utility.isDateToday(date)) { 442 text = mTimeFormat.format(date); 443 } else { 444 text = mDateFormat.format(date); 445 } 446 dateView.setText(text); 447 448 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 449 boolean selected = mChecked.contains(Long.valueOf(cursor.getLong(COLUMN_ID))); 450 selectedView.setImageDrawable(selected ? mSelectedIconOn : mSelectedIconOff); 451 452 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 453 boolean favorite = cursor.getInt(COLUMN_FAVORITE) != 0; 454 favoriteView.setImageDrawable(favorite ? mFavoriteIconOn : mFavoriteIconOff); 455 } 456 457 @Override 458 public View newView(Context context, Cursor cursor, ViewGroup parent) { 459 // TODO: This should be a custom view so we can deal with touch events 460 // in the checkbox & star. 461 return mInflater.inflate(R.layout.message_list_item, parent, false); 462 } 463 } 464 465 466} 467