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