MessageList.java revision a396a9bf1492febcb90c89faf1c0528a94bf5eaa
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 // Intent extras (internal to this activity) 62 private static final String EXTRA_MAILBOX_ID = "com.android.email.activity.MAILBOX_ID"; 63 private static final String EXTRA_ACCOUNT_NAME = "com.android.email.activity.ACCOUNT_NAME"; 64 private static final String EXTRA_MAILBOX_NAME = "com.android.email.activity.MAILBOX_NAME"; 65 66 // UI support 67 private ListView mListView; 68 private MessageListAdapter mListAdapter; 69 private MessageListHandler mHandler = new MessageListHandler(); 70 private ControllerResults mControllerCallback = new ControllerResults(); 71 72 // DB access 73 private long mMailboxId; 74 private LoadMessagesTask mLoadMessagesTask; 75 76 /** 77 * Open a specific mailbox. 78 * 79 * TODO This should just shortcut to a more generic version that can accept a list of 80 * accounts/mailboxes (e.g. merged inboxes). 81 * 82 * @param context 83 * @param id mailbox key 84 * @param accountName the account we're viewing 85 * @param mailboxName the mailbox we're viewing 86 */ 87 public static void actionHandleAccount(Context context, long id, 88 String accountName, String mailboxName) { 89 Intent intent = new Intent(context, MessageList.class); 90 intent.putExtra(EXTRA_MAILBOX_ID, id); 91 intent.putExtra(EXTRA_ACCOUNT_NAME, accountName); 92 intent.putExtra(EXTRA_MAILBOX_NAME, mailboxName); 93 context.startActivity(intent); 94 } 95 96 @Override 97 public void onCreate(Bundle icicle) { 98 super.onCreate(icicle); 99 100 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 101 102 setContentView(R.layout.message_list); 103 mListView = getListView(); 104 mListView.setOnItemClickListener(this); 105 mListView.setItemsCanFocus(false); 106 registerForContextMenu(mListView); 107 108 mListAdapter = new MessageListAdapter(this); 109 setListAdapter(mListAdapter); 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 } else { 264 MessageView.actionView(this, messageId); 265 } 266 } 267 268 private void onDelete(long messageId) { 269 Controller.getInstance(getApplication()).deleteMessage(messageId, -1); 270 Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); 271 } 272 273 private void onToggleRead(long messageId, View itemView) { 274 // TODO the read-unread state of the given message should be cached in the listview item. 275 // Instead, we're going to pull it from the DB here (expensively and in the wrong thread) 276 EmailContent.Message message = EmailContent.Message.restoreMessageWithId(this, messageId); 277 boolean isRead = ! message.mFlagRead; 278 279 // TODO this should be a call to the controller, since it may possibly kick off 280 // more than just a DB update. Also, the DB update shouldn't be in the UI thread 281 // as it is here. 282 ContentValues cv = new ContentValues(); 283 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead ? 1 : 0); 284 Uri uri = ContentUris.withAppendedId( 285 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 286 getContentResolver().update(uri, cv, null, null); 287 } 288 289 /** 290 * Async task for loading a single folder out of the UI thread 291 * 292 * TODO: Extend API to support compound select (e.g. merged inbox list) 293 */ 294 private class LoadMessagesTask extends AsyncTask<Void, Void, Cursor> { 295 296 private long mMailboxKey; 297 298 /** 299 * Special constructor to cache some local info 300 */ 301 public LoadMessagesTask(long mailboxKey) { 302 mMailboxKey = mailboxKey; 303 } 304 305 @Override 306 protected Cursor doInBackground(Void... params) { 307 return MessageList.this.managedQuery( 308 EmailContent.Message.CONTENT_URI, 309 MessageListAdapter.PROJECTION, 310 EmailContent.MessageColumns.MAILBOX_KEY + "=?", 311 new String[] { 312 String.valueOf(mMailboxKey) 313 }, 314 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 315 } 316 317 @Override 318 protected void onPostExecute(Cursor cursor) { 319 MessageList.this.mListAdapter.changeCursor(cursor); 320 321 // TODO: remove this hack and only update at the right time 322 if (cursor != null && cursor.getCount() == 0) { 323 onRefresh(); 324 } 325 } 326 } 327 328 /** 329 * Handler for UI-thread operations (when called from callbacks or any other threads) 330 */ 331 class MessageListHandler extends Handler { 332 private static final int MSG_PROGRESS = 1; 333 334 @Override 335 public void handleMessage(android.os.Message msg) { 336 switch (msg.what) { 337 case MSG_PROGRESS: 338 setProgressBarIndeterminateVisibility(msg.arg1 != 0); 339 break; 340 default: 341 super.handleMessage(msg); 342 } 343 } 344 345 public void progress(boolean progress) { 346 android.os.Message msg = android.os.Message.obtain(); 347 msg.what = MSG_PROGRESS; 348 msg.arg1 = progress ? 1 : 0; 349 sendMessage(msg); 350 } 351 } 352 353 /** 354 * Callback for async Controller results. This is all a placeholder until we figure out the 355 * final way to do this. 356 */ 357 private class ControllerResults implements Controller.Result { 358 public void updateMailboxListCallback(MessagingException result, long accountKey) { 359 } 360 361 public void updateMailboxCallback(MessagingException result, long accountKey, 362 long mailboxKey, int totalMessagesInMailbox, int numNewMessages) { 363 mHandler.progress(false); 364 } 365 } 366 367 /** 368 * This class implements the adapter for displaying messages based on cursors. 369 */ 370 private static class MessageListAdapter extends CursorAdapter { 371 372 public static final int COLUMN_ID = 0; 373 public static final int COLUMN_MAILBOX_KEY = 1; 374 public static final int COLUMN_DISPLAY_NAME = 2; 375 public static final int COLUMN_SUBJECT = 3; 376 public static final int COLUMN_DATE = 4; 377 public static final int COLUMN_READ = 5; 378 public static final int COLUMN_FAVORITE = 6; 379 public static final int COLUMN_ATTACHMENTS = 7; 380 381 public static final String[] PROJECTION = new String[] { 382 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, 383 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 384 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 385 }; 386 387 Context mContext; 388 private LayoutInflater mInflater; 389 private Drawable mAttachmentIcon; 390 private Drawable mFavoriteIconOn; 391 private Drawable mFavoriteIconOff; 392 private Drawable mSelectedIconOn; 393 private Drawable mSelectedIconOff; 394 395 private java.text.DateFormat mDateFormat; 396 private java.text.DateFormat mDayFormat; 397 private java.text.DateFormat mTimeFormat; 398 399 private HashSet<Long> mChecked = new HashSet<Long>(); 400 401 public MessageListAdapter(Context context) { 402 super(context, null); 403 mContext = context; 404 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 405 406 Resources resources = context.getResources(); 407 mAttachmentIcon = resources.getDrawable(R.drawable.ic_mms_attachment_small); 408 mFavoriteIconOn = resources.getDrawable(android.R.drawable.star_on); 409 mFavoriteIconOff = resources.getDrawable(android.R.drawable.star_off); 410 mSelectedIconOn = resources.getDrawable(R.drawable.btn_check_buttonless_on); 411 mSelectedIconOff = resources.getDrawable(R.drawable.btn_check_buttonless_off); 412 413 mDateFormat = android.text.format.DateFormat.getDateFormat(context); // short date 414 mDayFormat = android.text.format.DateFormat.getDateFormat(context); // TODO: day 415 mTimeFormat = android.text.format.DateFormat.getTimeFormat(context); // 12/24 time 416 } 417 418 @Override 419 public void bindView(View view, Context context, Cursor cursor) { 420 View clipView = view.findViewById(R.id.chip); 421 boolean readFlag = cursor.getInt(COLUMN_READ) != 0; 422 clipView.getBackground().setAlpha(readFlag ? 0 : 255); 423 424 TextView fromView = (TextView) view.findViewById(R.id.from); 425 String text = cursor.getString(COLUMN_DISPLAY_NAME); 426 if (text != null) fromView.setText(text); 427 428 boolean hasAttachments = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 429 fromView.setCompoundDrawablesWithIntrinsicBounds(null, null, 430 hasAttachments ? mAttachmentIcon : null, null); 431 432 TextView subjectView = (TextView) view.findViewById(R.id.subject); 433 text = cursor.getString(COLUMN_SUBJECT); 434 if (text != null) subjectView.setText(text); 435 436 // TODO ui spec suggests "time", "day", "date" - implement "day" 437 TextView dateView = (TextView) view.findViewById(R.id.date); 438 long timestamp = cursor.getLong(COLUMN_DATE); 439 Date date = new Date(timestamp); 440 if (Utility.isDateToday(date)) { 441 text = mTimeFormat.format(date); 442 } else { 443 text = mDateFormat.format(date); 444 } 445 dateView.setText(text); 446 447 ImageView selectedView = (ImageView) view.findViewById(R.id.selected); 448 boolean selected = mChecked.contains(Long.valueOf(cursor.getLong(COLUMN_ID))); 449 selectedView.setImageDrawable(selected ? mSelectedIconOn : mSelectedIconOff); 450 451 ImageView favoriteView = (ImageView) view.findViewById(R.id.favorite); 452 boolean favorite = cursor.getInt(COLUMN_FAVORITE) != 0; 453 favoriteView.setImageDrawable(favorite ? mFavoriteIconOn : mFavoriteIconOff); 454 } 455 456 @Override 457 public View newView(Context context, Cursor cursor, ViewGroup parent) { 458 return mInflater.inflate(R.layout.message_list_item, parent, false); 459 } 460 } 461 462 463} 464