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