ConversationList.java revision 07ce1878a36d2df1707dd4bbd9cd7235679bdc94
1/*
2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 2008 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mms.ui;
19
20import com.android.mms.R;
21import com.android.mms.data.Contact;
22import com.android.mms.data.ContactList;
23import com.android.mms.data.Conversation;
24import com.android.mms.transaction.MessagingNotification;
25import com.android.mms.util.DraftCache;
26
27import com.google.android.mms.pdu.PduHeaders;
28import com.google.android.mms.util.SqliteWrapper;
29
30import android.app.AlertDialog;
31import android.app.ListActivity;
32import android.content.AsyncQueryHandler;
33import android.content.ContentResolver;
34import android.content.DialogInterface;
35import android.content.Intent;
36import android.content.DialogInterface.OnClickListener;
37import android.content.res.Configuration;
38import android.database.Cursor;
39import android.database.sqlite.SQLiteException;
40import android.os.Bundle;
41import android.provider.Contacts;
42import android.provider.Contacts.Intents.Insert;
43import android.provider.Telephony.Mms;
44import android.util.Log;
45import android.view.ContextMenu;
46import android.view.KeyEvent;
47import android.view.LayoutInflater;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.View;
51import android.view.Window;
52import android.view.ContextMenu.ContextMenuInfo;
53import android.view.View.OnCreateContextMenuListener;
54import android.view.View.OnKeyListener;
55import android.widget.AdapterView;
56import android.widget.ListView;
57
58/**
59 * This activity provides a list view of existing conversations.
60 */
61public class ConversationList extends ListActivity
62            implements DraftCache.OnDraftChangedListener {
63    private static final String TAG = "ConversationList";
64    private static final boolean DEBUG = false;
65    private static final boolean LOCAL_LOGV = DEBUG;
66
67    private static final int THREAD_LIST_QUERY_TOKEN = 1701;
68
69    private static final int DELETE_CONVERSATION_TOKEN = 1801;
70
71    // IDs of the main menu items.
72    public static final int MENU_COMPOSE_NEW          = 0;
73    public static final int MENU_SEARCH               = 1;
74    public static final int MENU_DELETE_ALL           = 3;
75    public static final int MENU_PREFERENCES          = 4;
76
77    // IDs of the context menu items for the list of conversations.
78    public static final int MENU_DELETE               = 0;
79    public static final int MENU_VIEW                 = 1;
80    public static final int MENU_VIEW_CONTACT         = 2;
81    public static final int MENU_ADD_TO_CONTACTS      = 3;
82
83    private ThreadListQueryHandler mQueryHandler;
84    private ConversationListAdapter mListAdapter;
85    private CharSequence mTitle;
86
87    @Override
88    protected void onCreate(Bundle savedInstanceState) {
89        super.onCreate(savedInstanceState);
90
91        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
92        setContentView(R.layout.conversation_list_screen);
93
94        mQueryHandler = new ThreadListQueryHandler(getContentResolver());
95
96        ListView listView = getListView();
97        LayoutInflater inflater = LayoutInflater.from(this);
98        ConversationHeaderView headerView = (ConversationHeaderView)
99                inflater.inflate(R.layout.conversation_header, listView, false);
100        headerView.bind(getString(R.string.new_message),
101                getString(R.string.create_new_message));
102        listView.addHeaderView(headerView, null, true);
103
104        listView.setOnCreateContextMenuListener(mConvListOnCreateContextMenuListener);
105        listView.setOnKeyListener(mThreadListKeyListener);
106
107        initListAdapter();
108
109        handleCreationIntent(getIntent());
110    }
111
112    private void initListAdapter() {
113        mListAdapter = new ConversationListAdapter(this, null);
114        setListAdapter(mListAdapter);
115    }
116
117    @Override
118    protected void onNewIntent(Intent intent) {
119        // Handle intents that occur after the activity has already been created.
120        handleCreationIntent(intent);
121    }
122
123    protected void handleCreationIntent(Intent intent) {
124        // Handle intents that occur upon creation of the activity.
125        initNormalQueryArgs();
126   }
127
128    @Override
129    protected void onResume() {
130        super.onResume();
131
132        DraftCache.getInstance().addOnDraftChangedListener(this);
133
134        Conversation.cleanup(this);
135
136        // Make sure the draft cache is up to date.
137        DraftCache.getInstance().refresh();
138
139        startAsyncQuery();
140
141        Contact.invalidateCache();
142    }
143
144    @Override
145    protected void onPause() {
146        super.onPause();
147
148        DraftCache.getInstance().removeOnDraftChangedListener(this);
149    }
150
151    @Override
152    protected void onStop() {
153        super.onStop();
154
155        mListAdapter.changeCursor(null);
156    }
157
158    public void onDraftChanged(long threadId, boolean hasDraft) {
159        // Run notifyDataSetChanged() on the main thread.
160        mQueryHandler.post(new Runnable() {
161            public void run() {
162                mListAdapter.notifyDataSetChanged();
163            }
164        });
165    }
166
167    private void initNormalQueryArgs() {
168        mTitle = getString(R.string.app_label);
169    }
170
171    private void startAsyncQuery() {
172        try {
173            setTitle(getString(R.string.refreshing));
174            setProgressBarIndeterminateVisibility(true);
175
176            Conversation.startQueryForAll(mQueryHandler, THREAD_LIST_QUERY_TOKEN);
177        } catch (SQLiteException e) {
178            SqliteWrapper.checkSQLiteException(this, e);
179        }
180    }
181
182    @Override
183    public boolean onPrepareOptionsMenu(Menu menu) {
184        menu.clear();
185
186        menu.add(0, MENU_COMPOSE_NEW, 0, R.string.menu_compose_new).setIcon(
187                com.android.internal.R.drawable.ic_menu_compose);
188
189        if (mListAdapter.getCount() > 0) {
190            menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon(
191                    android.R.drawable.ic_menu_delete);
192        }
193
194        menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
195                android.R.drawable.ic_menu_preferences);
196
197        menu.add(0, MENU_SEARCH, 0, android.R.string.search_go).
198                setIcon(android.R.drawable.ic_menu_search).
199                setAlphabeticShortcut(android.app.SearchManager.MENU_KEY);
200
201        return true;
202    }
203
204    @Override
205    public boolean onSearchRequested() {
206        startSearch(null, false, null /*appData*/, false);
207        return true;
208    }
209
210    @Override
211    public boolean onOptionsItemSelected(MenuItem item) {
212        switch(item.getItemId()) {
213            case MENU_COMPOSE_NEW:
214                createNewMessage();
215                break;
216            case MENU_SEARCH:
217                onSearchRequested();
218                break;
219            case MENU_DELETE_ALL:
220                confirmDeleteDialog(new DeleteThreadListener(-1L), true);
221                break;
222            case MENU_PREFERENCES: {
223                Intent intent = new Intent(this, MessagingPreferenceActivity.class);
224                startActivityIfNeeded(intent, -1);
225                break;
226            }
227            default:
228                return true;
229        }
230        return false;
231    }
232
233    @Override
234    protected void onListItemClick(ListView l, View v, int position, long id) {
235        if (LOCAL_LOGV) {
236            Log.v(TAG, "onListItemClick: position=" + position + ", id=" + id);
237        }
238
239        if (position == 0) {
240            createNewMessage();
241        } else if (v instanceof ConversationHeaderView) {
242            ConversationHeaderView headerView = (ConversationHeaderView) v;
243            ConversationHeader ch = headerView.getConversationHeader();
244            openThread(ch.getThreadId());
245        }
246    }
247
248    private void createNewMessage() {
249        Intent intent = new Intent(this, ComposeMessageActivity.class);
250        startActivity(intent);
251    }
252
253    private void openThread(long threadId) {
254        Intent intent = new Intent(this, ComposeMessageActivity.class);
255        intent.putExtra("thread_id", threadId);
256        startActivity(intent);
257    }
258
259    public static Intent createAddContactIntent(String address) {
260        // address must be a single recipient
261        Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
262        intent.setType(Contacts.People.CONTENT_ITEM_TYPE);
263        if (Mms.isEmailAddress(address)) {
264            intent.putExtra(Insert.EMAIL, address);
265        } else {
266            intent.putExtra(Insert.PHONE, address);
267        }
268
269        return intent;
270    }
271
272    private final OnCreateContextMenuListener mConvListOnCreateContextMenuListener =
273        new OnCreateContextMenuListener() {
274        public void onCreateContextMenu(ContextMenu menu, View v,
275                ContextMenuInfo menuInfo) {
276            Cursor cursor = mListAdapter.getCursor();
277            Conversation conv = Conversation.from(ConversationList.this, cursor);
278            ContactList recipients = conv.getRecipients();
279            menu.setHeaderTitle(recipients.formatNames(","));
280
281            AdapterView.AdapterContextMenuInfo info =
282                (AdapterView.AdapterContextMenuInfo) menuInfo;
283            if (info.position > 0) {
284                menu.add(0, MENU_VIEW, 0, R.string.menu_view);
285
286                // Only show if there's a single recipient
287                if (recipients.size() == 1) {
288                    // do we have this recipient in contacts?
289                    if (recipients.get(0).existsInDatabase()) {
290                        menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact);
291                    } else {
292                        menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts);
293                    }
294                }
295                menu.add(0, MENU_DELETE, 0, R.string.menu_delete);
296            }
297        }
298    };
299
300    @Override
301    public boolean onContextItemSelected(MenuItem item) {
302        Cursor cursor = mListAdapter.getCursor();
303        Conversation conv = Conversation.from(ConversationList.this, cursor);
304        long threadId = conv.getThreadId();
305        switch (item.getItemId()) {
306            case MENU_DELETE: {
307                DeleteThreadListener l = new DeleteThreadListener(threadId);
308                confirmDeleteDialog(l, false);
309                break;
310            }
311            case MENU_VIEW: {
312                openThread(threadId);
313                break;
314            }
315            case MENU_VIEW_CONTACT: {
316                Contact contact = conv.getRecipients().get(0);
317                Intent intent = new Intent(Intent.ACTION_VIEW, contact.getUri());
318                startActivity(intent);
319                break;
320            }
321            case MENU_ADD_TO_CONTACTS: {
322                String address = conv.getRecipients().get(0).getNumber();
323                startActivity(createAddContactIntent(address));
324                break;
325            }
326            default:
327                break;
328        }
329
330        return super.onContextItemSelected(item);
331    }
332
333    public void onConfigurationChanged(Configuration newConfig) {
334        // We override this method to avoid restarting the entire
335        // activity when the keyboard is opened (declared in
336        // AndroidManifest.xml).  Because the only translatable text
337        // in this activity is "New Message", which has the full width
338        // of phone to work with, localization shouldn't be a problem:
339        // no abbreviated alternate words should be needed even in
340        // 'wide' languages like German or Russian.
341
342        super.onConfigurationChanged(newConfig);
343        if (DEBUG) Log.v(TAG, "onConfigurationChanged: " + newConfig);
344    }
345
346    private void confirmDeleteDialog(OnClickListener listener, boolean deleteAll) {
347        AlertDialog.Builder builder = new AlertDialog.Builder(this);
348        builder.setTitle(R.string.confirm_dialog_title);
349        builder.setIcon(android.R.drawable.ic_dialog_alert);
350        builder.setCancelable(true);
351        builder.setPositiveButton(R.string.yes, listener);
352        builder.setNegativeButton(R.string.no, null);
353        builder.setMessage(deleteAll
354                ? R.string.confirm_delete_all_conversations
355                : R.string.confirm_delete_conversation);
356
357        builder.show();
358    }
359
360    private final OnKeyListener mThreadListKeyListener = new OnKeyListener() {
361        public boolean onKey(View v, int keyCode, KeyEvent event) {
362            if (event.getAction() == KeyEvent.ACTION_DOWN) {
363                switch (keyCode) {
364                    case KeyEvent.KEYCODE_DEL: {
365                        long id = getListView().getSelectedItemId();
366                        if (id > 0) {
367                            DeleteThreadListener l = new DeleteThreadListener(
368                                    id);
369                            confirmDeleteDialog(l, false);
370                        }
371                        return true;
372                    }
373                }
374            }
375            return false;
376        }
377    };
378
379    private class DeleteThreadListener implements OnClickListener {
380        private final long mThreadId;
381
382        public DeleteThreadListener(long threadId) {
383            mThreadId = threadId;
384        }
385
386        public void onClick(DialogInterface dialog, int whichButton) {
387            MessageUtils.handleReadReport(ConversationList.this, mThreadId,
388                    PduHeaders.READ_STATUS__DELETED_WITHOUT_BEING_READ, new Runnable() {
389                public void run() {
390                    int token = DELETE_CONVERSATION_TOKEN;
391                    if (mThreadId == -1) {
392                        Conversation.startDeleteAll(mQueryHandler, token);
393                    } else {
394                        Conversation.startDelete(mQueryHandler, token, mThreadId);
395                    }
396                }
397            });
398        }
399    }
400
401    private final class ThreadListQueryHandler extends AsyncQueryHandler {
402        public ThreadListQueryHandler(ContentResolver contentResolver) {
403            super(contentResolver);
404        }
405
406        @Override
407        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
408            switch (token) {
409            case THREAD_LIST_QUERY_TOKEN:
410                mListAdapter.changeCursor(cursor);
411                setTitle(mTitle);
412                setProgressBarIndeterminateVisibility(false);
413                break;
414            default:
415                Log.e(TAG, "onQueryComplete called with unknown token " + token);
416            }
417        }
418
419        @Override
420        protected void onDeleteComplete(int token, Object cookie, int result) {
421            switch (token) {
422            case DELETE_CONVERSATION_TOKEN:
423                // Make sure the conversation cache reflects the threads in the DB.
424                Conversation.init(ConversationList.this);
425
426                // Update the notification for new messages since they
427                // may be deleted.
428                MessagingNotification.updateNewMessageIndicator(ConversationList.this);
429                // Update the notification for failed messages since they
430                // may be deleted.
431                MessagingNotification.updateSendFailedNotification(ConversationList.this);
432
433                // Make sure the list reflects the delete
434                startAsyncQuery();
435
436                onContentChanged();
437                break;
438            }
439        }
440    }
441}
442