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