1/*
2 * Copyright (C) 2007 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.contacts.activities;
18
19import android.app.ActionBar;
20import android.app.ActionBar.LayoutParams;
21import android.app.Activity;
22import android.app.Fragment;
23import android.content.ActivityNotFoundException;
24import android.content.Context;
25import android.content.Intent;
26import android.net.Uri;
27import android.os.Bundle;
28import android.provider.ContactsContract.Contacts;
29import android.provider.ContactsContract.Intents.Insert;
30import android.provider.ContactsContract.Intents.UI;
31import android.text.TextUtils;
32import android.util.Log;
33import android.view.LayoutInflater;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.View.OnFocusChangeListener;
40import android.view.inputmethod.InputMethodManager;
41import android.widget.SearchView;
42import android.widget.SearchView.OnCloseListener;
43import android.widget.SearchView.OnQueryTextListener;
44import android.widget.Toast;
45
46import com.android.contacts.ContactsActivity;
47import com.android.contacts.R;
48import com.android.contacts.common.list.ContactEntryListFragment;
49import com.android.contacts.list.ContactPickerFragment;
50import com.android.contacts.list.ContactsIntentResolver;
51import com.android.contacts.list.ContactsRequest;
52import com.android.contacts.common.list.DirectoryListLoader;
53import com.android.contacts.list.EmailAddressPickerFragment;
54import com.android.contacts.list.JoinContactListFragment;
55import com.android.contacts.list.LegacyPhoneNumberPickerFragment;
56import com.android.contacts.list.OnContactPickerActionListener;
57import com.android.contacts.list.OnEmailAddressPickerActionListener;
58import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
59import com.android.contacts.list.OnPostalAddressPickerActionListener;
60import com.android.contacts.common.list.PhoneNumberPickerFragment;
61import com.android.contacts.list.PostalAddressPickerFragment;
62import com.google.common.collect.Sets;
63
64import java.util.Set;
65
66/**
67 * Displays a list of contacts (or phone numbers or postal addresses) for the
68 * purposes of selecting one.
69 */
70public class ContactSelectionActivity extends ContactsActivity
71        implements View.OnCreateContextMenuListener, OnQueryTextListener, OnClickListener,
72                OnCloseListener, OnFocusChangeListener {
73    private static final String TAG = "ContactSelectionActivity";
74
75    private static final int SUBACTIVITY_ADD_TO_EXISTING_CONTACT = 0;
76
77    private static final String KEY_ACTION_CODE = "actionCode";
78    private static final String KEY_SEARCH_MODE = "searchMode";
79    private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
80
81    private ContactsIntentResolver mIntentResolver;
82    protected ContactEntryListFragment<?> mListFragment;
83
84    private int mActionCode = -1;
85    private boolean mIsSearchMode;
86
87    private ContactsRequest mRequest;
88    private SearchView mSearchView;
89    private View mSearchViewContainer;
90
91    public ContactSelectionActivity() {
92        mIntentResolver = new ContactsIntentResolver(this);
93    }
94
95    @Override
96    public void onAttachFragment(Fragment fragment) {
97        if (fragment instanceof ContactEntryListFragment<?>) {
98            mListFragment = (ContactEntryListFragment<?>) fragment;
99            setupActionListener();
100        }
101    }
102
103    @Override
104    protected void onCreate(Bundle savedState) {
105        super.onCreate(savedState);
106
107        if (savedState != null) {
108            mActionCode = savedState.getInt(KEY_ACTION_CODE);
109            mIsSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
110        }
111
112        // Extract relevant information from the intent
113        mRequest = mIntentResolver.resolveIntent(getIntent());
114        if (!mRequest.isValid()) {
115            setResult(RESULT_CANCELED);
116            finish();
117            return;
118        }
119
120        Intent redirect = mRequest.getRedirectIntent();
121        if (redirect != null) {
122            // Need to start a different activity
123            startActivity(redirect);
124            finish();
125            return;
126        }
127
128        configureActivityTitle();
129
130        setContentView(R.layout.contact_picker);
131
132        if (mActionCode != mRequest.getActionCode()) {
133            mActionCode = mRequest.getActionCode();
134            configureListFragment();
135        }
136
137        prepareSearchViewAndActionBar();
138    }
139
140    private void prepareSearchViewAndActionBar() {
141        final ActionBar actionBar = getActionBar();
142        mSearchViewContainer = LayoutInflater.from(actionBar.getThemedContext())
143                .inflate(R.layout.custom_action_bar, null);
144        mSearchView = (SearchView) mSearchViewContainer.findViewById(R.id.search_view);
145
146        // Postal address pickers (and legacy pickers) don't support search, so just show
147        // "HomeAsUp" button and title.
148        if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL ||
149                mRequest.isLegacyCompatibilityMode()) {
150            mSearchView.setVisibility(View.GONE);
151            if (actionBar != null) {
152                actionBar.setDisplayShowHomeEnabled(true);
153                actionBar.setDisplayHomeAsUpEnabled(true);
154                actionBar.setDisplayShowTitleEnabled(true);
155            }
156            return;
157        }
158
159        actionBar.setDisplayShowHomeEnabled(true);
160        actionBar.setDisplayHomeAsUpEnabled(true);
161
162        // In order to make the SearchView look like "shown via search menu", we need to
163        // manually setup its state. See also DialtactsActivity.java and ActionBarAdapter.java.
164        mSearchView.setIconifiedByDefault(true);
165        mSearchView.setQueryHint(getString(R.string.hint_findContacts));
166        mSearchView.setIconified(false);
167        mSearchView.setFocusable(true);
168
169        mSearchView.setOnQueryTextListener(this);
170        mSearchView.setOnCloseListener(this);
171        mSearchView.setOnQueryTextFocusChangeListener(this);
172
173        actionBar.setCustomView(mSearchViewContainer,
174                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
175        actionBar.setDisplayShowCustomEnabled(true);
176
177        configureSearchMode();
178    }
179
180    private void configureSearchMode() {
181        final ActionBar actionBar = getActionBar();
182        if (mIsSearchMode) {
183            actionBar.setDisplayShowTitleEnabled(false);
184            mSearchViewContainer.setVisibility(View.VISIBLE);
185            mSearchView.requestFocus();
186        } else {
187            actionBar.setDisplayShowTitleEnabled(true);
188            mSearchViewContainer.setVisibility(View.GONE);
189            mSearchView.setQuery(null, true);
190        }
191        invalidateOptionsMenu();
192    }
193
194    @Override
195    public boolean onOptionsItemSelected(MenuItem item) {
196        switch (item.getItemId()) {
197            case android.R.id.home:
198                // Go back to previous screen, intending "cancel"
199                setResult(RESULT_CANCELED);
200                onBackPressed();
201                return true;
202            case R.id.menu_search:
203                mIsSearchMode = !mIsSearchMode;
204                configureSearchMode();
205                return true;
206        }
207        return super.onOptionsItemSelected(item);
208    }
209
210    @Override
211    protected void onSaveInstanceState(Bundle outState) {
212        super.onSaveInstanceState(outState);
213        outState.putInt(KEY_ACTION_CODE, mActionCode);
214        outState.putBoolean(KEY_SEARCH_MODE, mIsSearchMode);
215    }
216
217    private void configureActivityTitle() {
218        if (!TextUtils.isEmpty(mRequest.getActivityTitle())) {
219            setTitle(mRequest.getActivityTitle());
220            return;
221        }
222
223        int actionCode = mRequest.getActionCode();
224        switch (actionCode) {
225            case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: {
226                setTitle(R.string.contactPickerActivityTitle);
227                break;
228            }
229
230            case ContactsRequest.ACTION_PICK_CONTACT: {
231                setTitle(R.string.contactPickerActivityTitle);
232                break;
233            }
234
235            case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: {
236                setTitle(R.string.contactPickerActivityTitle);
237                break;
238            }
239
240            case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: {
241                setTitle(R.string.shortcutActivityTitle);
242                break;
243            }
244
245            case ContactsRequest.ACTION_PICK_PHONE: {
246                setTitle(R.string.contactPickerActivityTitle);
247                break;
248            }
249
250            case ContactsRequest.ACTION_PICK_EMAIL: {
251                setTitle(R.string.contactPickerActivityTitle);
252                break;
253            }
254
255            case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
256                setTitle(R.string.callShortcutActivityTitle);
257                break;
258            }
259
260            case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: {
261                setTitle(R.string.messageShortcutActivityTitle);
262                break;
263            }
264
265            case ContactsRequest.ACTION_PICK_POSTAL: {
266                setTitle(R.string.contactPickerActivityTitle);
267                break;
268            }
269
270            case ContactsRequest.ACTION_PICK_JOIN: {
271                setTitle(R.string.titleJoinContactDataWith);
272                break;
273            }
274        }
275    }
276
277    /**
278     * Creates the fragment based on the current request.
279     */
280    public void configureListFragment() {
281        switch (mActionCode) {
282            case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: {
283                ContactPickerFragment fragment = new ContactPickerFragment();
284                fragment.setEditMode(true);
285                fragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
286                fragment.setCreateContactEnabled(!mRequest.isSearchMode());
287                mListFragment = fragment;
288                break;
289            }
290
291            case ContactsRequest.ACTION_DEFAULT:
292            case ContactsRequest.ACTION_PICK_CONTACT: {
293                ContactPickerFragment fragment = new ContactPickerFragment();
294                fragment.setIncludeProfile(mRequest.shouldIncludeProfile());
295                mListFragment = fragment;
296                break;
297            }
298
299            case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: {
300                ContactPickerFragment fragment = new ContactPickerFragment();
301                fragment.setCreateContactEnabled(!mRequest.isSearchMode());
302                mListFragment = fragment;
303                break;
304            }
305
306            case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: {
307                ContactPickerFragment fragment = new ContactPickerFragment();
308                fragment.setShortcutRequested(true);
309                mListFragment = fragment;
310                break;
311            }
312
313            case ContactsRequest.ACTION_PICK_PHONE: {
314                PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest);
315                mListFragment = fragment;
316                break;
317            }
318
319            case ContactsRequest.ACTION_PICK_EMAIL: {
320                mListFragment = new EmailAddressPickerFragment();
321                break;
322            }
323
324            case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: {
325                PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest);
326                fragment.setShortcutAction(Intent.ACTION_CALL);
327
328                mListFragment = fragment;
329                break;
330            }
331
332            case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: {
333                PhoneNumberPickerFragment fragment = getPhoneNumberPickerFragment(mRequest);
334                fragment.setShortcutAction(Intent.ACTION_SENDTO);
335
336                mListFragment = fragment;
337                break;
338            }
339
340            case ContactsRequest.ACTION_PICK_POSTAL: {
341                PostalAddressPickerFragment fragment = new PostalAddressPickerFragment();
342
343                mListFragment = fragment;
344                break;
345            }
346
347            case ContactsRequest.ACTION_PICK_JOIN: {
348                JoinContactListFragment joinFragment = new JoinContactListFragment();
349                joinFragment.setTargetContactId(getTargetContactId());
350                mListFragment = joinFragment;
351                break;
352            }
353
354            default:
355                throw new IllegalStateException("Invalid action code: " + mActionCode);
356        }
357
358        // Setting compatibility is no longer needed for PhoneNumberPickerFragment since that logic
359        // has been separated into LegacyPhoneNumberPickerFragment.  But we still need to set
360        // compatibility for other fragments.
361        mListFragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode());
362        mListFragment.setDirectoryResultLimit(DEFAULT_DIRECTORY_RESULT_LIMIT);
363
364        getFragmentManager().beginTransaction()
365                .replace(R.id.list_container, mListFragment)
366                .commitAllowingStateLoss();
367    }
368
369    private PhoneNumberPickerFragment getPhoneNumberPickerFragment(ContactsRequest request) {
370        if (mRequest.isLegacyCompatibilityMode()) {
371            return new LegacyPhoneNumberPickerFragment();
372        } else {
373            return new PhoneNumberPickerFragment();
374        }
375    }
376
377    public void setupActionListener() {
378        if (mListFragment instanceof ContactPickerFragment) {
379            ((ContactPickerFragment) mListFragment).setOnContactPickerActionListener(
380                    new ContactPickerActionListener());
381        } else if (mListFragment instanceof PhoneNumberPickerFragment) {
382            ((PhoneNumberPickerFragment) mListFragment).setOnPhoneNumberPickerActionListener(
383                    new PhoneNumberPickerActionListener());
384        } else if (mListFragment instanceof PostalAddressPickerFragment) {
385            ((PostalAddressPickerFragment) mListFragment).setOnPostalAddressPickerActionListener(
386                    new PostalAddressPickerActionListener());
387        } else if (mListFragment instanceof EmailAddressPickerFragment) {
388            ((EmailAddressPickerFragment) mListFragment).setOnEmailAddressPickerActionListener(
389                    new EmailAddressPickerActionListener());
390        } else if (mListFragment instanceof JoinContactListFragment) {
391            ((JoinContactListFragment) mListFragment).setOnContactPickerActionListener(
392                    new JoinContactActionListener());
393        } else {
394            throw new IllegalStateException("Unsupported list fragment type: " + mListFragment);
395        }
396    }
397
398    private final class ContactPickerActionListener implements OnContactPickerActionListener {
399        @Override
400        public void onCreateNewContactAction() {
401            startCreateNewContactActivity();
402        }
403
404        @Override
405        public void onEditContactAction(Uri contactLookupUri) {
406            Bundle extras = getIntent().getExtras();
407            if (launchAddToContactDialog(extras)) {
408                // Show a confirmation dialog to add the value(s) to the existing contact.
409                Intent intent = new Intent(ContactSelectionActivity.this,
410                        ConfirmAddDetailActivity.class);
411                intent.setData(contactLookupUri);
412                if (extras != null) {
413                    // First remove name key if present because the dialog does not support name
414                    // editing. This is fine because the user wants to add information to an
415                    // existing contact, who should already have a name and we wouldn't want to
416                    // override the name.
417                    extras.remove(Insert.NAME);
418                    intent.putExtras(extras);
419                }
420
421                // Wait for the activity result because we want to keep the picker open (in case the
422                // user cancels adding the info to a contact and wants to pick someone else).
423                startActivityForResult(intent, SUBACTIVITY_ADD_TO_EXISTING_CONTACT);
424            } else {
425                // Otherwise launch the full contact editor.
426                startActivityAndForwardResult(new Intent(Intent.ACTION_EDIT, contactLookupUri));
427            }
428        }
429
430        @Override
431        public void onPickContactAction(Uri contactUri) {
432            returnPickerResult(contactUri);
433        }
434
435        @Override
436        public void onShortcutIntentCreated(Intent intent) {
437            returnPickerResult(intent);
438        }
439
440        /**
441         * Returns true if is a single email or single phone number provided in the {@link Intent}
442         * extras bundle so that a pop-up confirmation dialog can be used to add the data to
443         * a contact. Otherwise return false if there are other intent extras that require launching
444         * the full contact editor. Ignore extras with the key {@link Insert.NAME} because names
445         * are a special case and we typically don't want to replace the name of an existing
446         * contact.
447         */
448        private boolean launchAddToContactDialog(Bundle extras) {
449            if (extras == null) {
450                return false;
451            }
452
453            // Copy extras because the set may be modified in the next step
454            Set<String> intentExtraKeys = Sets.newHashSet();
455            intentExtraKeys.addAll(extras.keySet());
456
457            // Ignore name key because this is an existing contact.
458            if (intentExtraKeys.contains(Insert.NAME)) {
459                intentExtraKeys.remove(Insert.NAME);
460            }
461
462            int numIntentExtraKeys = intentExtraKeys.size();
463            if (numIntentExtraKeys == 2) {
464                boolean hasPhone = intentExtraKeys.contains(Insert.PHONE) &&
465                        intentExtraKeys.contains(Insert.PHONE_TYPE);
466                boolean hasEmail = intentExtraKeys.contains(Insert.EMAIL) &&
467                        intentExtraKeys.contains(Insert.EMAIL_TYPE);
468                return hasPhone || hasEmail;
469            } else if (numIntentExtraKeys == 1) {
470                return intentExtraKeys.contains(Insert.PHONE) ||
471                        intentExtraKeys.contains(Insert.EMAIL);
472            }
473            // Having 0 or more than 2 intent extra keys means that we should launch
474            // the full contact editor to properly handle the intent extras.
475            return false;
476        }
477    }
478
479    private final class PhoneNumberPickerActionListener implements
480            OnPhoneNumberPickerActionListener {
481        @Override
482        public void onPickPhoneNumberAction(Uri dataUri) {
483            returnPickerResult(dataUri);
484        }
485
486        @Override
487        public void onCallNumberDirectly(String phoneNumber) {
488            Log.w(TAG, "Unsupported call.");
489        }
490
491        @Override
492        public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) {
493            Log.w(TAG, "Unsupported call.");
494        }
495
496        @Override
497        public void onShortcutIntentCreated(Intent intent) {
498            returnPickerResult(intent);
499        }
500
501        public void onHomeInActionBarSelected() {
502            ContactSelectionActivity.this.onBackPressed();
503        }
504    }
505
506    private final class JoinContactActionListener implements OnContactPickerActionListener {
507        @Override
508        public void onPickContactAction(Uri contactUri) {
509            Intent intent = new Intent(null, contactUri);
510            setResult(RESULT_OK, intent);
511            finish();
512        }
513
514        @Override
515        public void onShortcutIntentCreated(Intent intent) {
516        }
517
518        @Override
519        public void onCreateNewContactAction() {
520        }
521
522        @Override
523        public void onEditContactAction(Uri contactLookupUri) {
524        }
525    }
526
527    private final class PostalAddressPickerActionListener implements
528            OnPostalAddressPickerActionListener {
529        @Override
530        public void onPickPostalAddressAction(Uri dataUri) {
531            returnPickerResult(dataUri);
532        }
533    }
534
535    private final class EmailAddressPickerActionListener implements
536            OnEmailAddressPickerActionListener {
537        @Override
538        public void onPickEmailAddressAction(Uri dataUri) {
539            returnPickerResult(dataUri);
540        }
541    }
542
543    public void startActivityAndForwardResult(final Intent intent) {
544        intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
545
546        // Forward extras to the new activity
547        Bundle extras = getIntent().getExtras();
548        if (extras != null) {
549            intent.putExtras(extras);
550        }
551        try {
552            startActivity(intent);
553        } catch (ActivityNotFoundException e) {
554            Log.e(TAG, "startActivity() failed: " + e);
555            Toast.makeText(ContactSelectionActivity.this, R.string.missing_app,
556                    Toast.LENGTH_SHORT).show();
557        }
558        finish();
559    }
560
561    @Override
562    public boolean onQueryTextChange(String newText) {
563        mListFragment.setQueryString(newText, true);
564        return false;
565    }
566
567    @Override
568    public boolean onQueryTextSubmit(String query) {
569        return false;
570    }
571
572    @Override
573    public boolean onClose() {
574        if (!TextUtils.isEmpty(mSearchView.getQuery())) {
575            mSearchView.setQuery(null, true);
576        }
577        return true;
578    }
579
580    @Override
581    public void onFocusChange(View view, boolean hasFocus) {
582        switch (view.getId()) {
583            case R.id.search_view: {
584                if (hasFocus) {
585                    showInputMethod(mSearchView.findFocus());
586                }
587            }
588        }
589    }
590
591    public void returnPickerResult(Uri data) {
592        Intent intent = new Intent();
593        intent.setData(data);
594        returnPickerResult(intent);
595    }
596
597    public void returnPickerResult(Intent intent) {
598        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
599        setResult(RESULT_OK, intent);
600        finish();
601    }
602
603    @Override
604    public void onClick(View view) {
605        switch (view.getId()) {
606            case R.id.floating_action_button: {
607                startCreateNewContactActivity();
608                break;
609            }
610        }
611    }
612
613    private long getTargetContactId() {
614        Intent intent = getIntent();
615        final long targetContactId = intent.getLongExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, -1);
616        if (targetContactId == -1) {
617            Log.e(TAG, "Intent " + intent.getAction() + " is missing required extra: "
618                    + UI.TARGET_CONTACT_ID_EXTRA_KEY);
619            setResult(RESULT_CANCELED);
620            finish();
621            return -1;
622        }
623        return targetContactId;
624    }
625
626    private void startCreateNewContactActivity() {
627        Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
628        intent.putExtra(ContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true);
629        startActivityAndForwardResult(intent);
630    }
631
632    private void showInputMethod(View view) {
633        final InputMethodManager imm = (InputMethodManager)
634                getSystemService(Context.INPUT_METHOD_SERVICE);
635        if (imm != null) {
636            if (!imm.showSoftInput(view, 0)) {
637                Log.w(TAG, "Failed to show soft input method.");
638            }
639        }
640    }
641
642    @Override
643    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
644        super.onActivityResult(requestCode, resultCode, data);
645        if (requestCode == SUBACTIVITY_ADD_TO_EXISTING_CONTACT) {
646            if (resultCode == Activity.RESULT_OK) {
647                if (data != null) {
648                    startActivity(data);
649                }
650                finish();
651            }
652        }
653    }
654
655    @Override
656    public boolean onCreateOptionsMenu(Menu menu) {
657        super.onCreateOptionsMenu(menu);
658
659        final MenuInflater inflater = getMenuInflater();
660        inflater.inflate(R.menu.search_menu, menu);
661
662        final MenuItem searchItem = menu.findItem(R.id.menu_search);
663        searchItem.setVisible(!mIsSearchMode);
664        return true;
665    }
666
667    @Override
668    public void onBackPressed() {
669        if (mIsSearchMode) {
670            mIsSearchMode = false;
671            configureSearchMode();
672        } else {
673            super.onBackPressed();
674        }
675    }
676}
677