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