/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.silentcircle.contacts.activities; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.inputmethod.InputMethodManager; //import com.android.contacts.ContactsActivity; import com.silentcircle.contacts.list.ContactsIntentResolver; import com.silentcircle.contacts.list.EmailAddressPickerFragment; import com.silentcircle.contacts.list.OnContactPickerActionListener; import com.silentcircle.contacts.list.OnEmailAddressPickerActionListener; import com.silentcircle.contacts.list.PhoneNumberPickerFragment; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract.Intents.Insert; import com.silentcircle.silentcontacts.ScContactsContract.RawContacts; import com.silentcircle.contacts.list.ScContactEntryListFragment; import com.silentcircle.contacts.list.ContactPickerFragment; import com.silentcircle.contacts.list.ContactsRequest; import com.silentcircle.contacts.list.DirectoryListLoader; import com.silentcircle.contacts.list.OnPhoneNumberPickerActionListener; //import com.silentcircle.silentcontacts.list.OnPostalAddressPickerActionListener; //import com.silentcircle.silentcontacts.list.PostalAddressPickerFragment; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.ActionBar.LayoutParams; import com.actionbarsherlock.app.SherlockFragmentActivity; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.actionbarsherlock.widget.SearchView; import com.actionbarsherlock.widget.SearchView.OnCloseListener; import com.actionbarsherlock.widget.SearchView.OnQueryTextListener; import com.google.common.collect.Sets; import java.util.Set; /** * Displays a list of contacts (or phone numbers or postal addresses) for the * purposes of selecting one. */ public class ScContactSelectionActivity extends SherlockFragmentActivity implements OnQueryTextListener, OnClickListener, OnCloseListener, OnFocusChangeListener { private static final String TAG = "ContactSelectionActivity"; private static final int SUBACTIVITY_ADD_TO_EXISTING_CONTACT = 0; private static final String KEY_ACTION_CODE = "actionCode"; private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; // Delay to allow the UI to settle before making search view visible private static final int FOCUS_DELAY = 200; private ContactsIntentResolver mIntentResolver; protected ScContactEntryListFragment<?> mListFragment; private int mActionCode = -1; private ContactsRequest mRequest; private SearchView mSearchView; /** * Can be null. If null, the "Create New Contact" button should be on the menu. */ private View mCreateNewContactButton; public ScContactSelectionActivity() { mIntentResolver = new ContactsIntentResolver(this); } @Override public void onAttachFragment(Fragment fragment) { if (fragment instanceof ScContactEntryListFragment<?>) { mListFragment = (ScContactEntryListFragment<?>) fragment; setupActionListener(); } } @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); if (savedState != null) { mActionCode = savedState.getInt(KEY_ACTION_CODE); } // Extract relevant information from the intent mRequest = mIntentResolver.resolveIntent(getIntent()); if (!mRequest.isValid()) { setResult(RESULT_CANCELED); finish(); return; } Intent redirect = mRequest.getRedirectIntent(); if (redirect != null) { // Need to start a different activity startActivity(redirect); finish(); return; } configureActivityTitle(); setContentView(R.layout.contact_picker); if (mActionCode != mRequest.getActionCode()) { mActionCode = mRequest.getActionCode(); configureListFragment(); } prepareSearchViewAndActionBar(); mCreateNewContactButton = findViewById(R.id.new_contact); if (mCreateNewContactButton != null) { if (shouldShowCreateNewContactButton()) { mCreateNewContactButton.setVisibility(View.VISIBLE); mCreateNewContactButton.setOnClickListener(this); } else { mCreateNewContactButton.setVisibility(View.GONE); } } } private boolean shouldShowCreateNewContactButton() { return (mActionCode == ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT || (mActionCode == ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT && !mRequest.isSearchMode())); } private void prepareSearchViewAndActionBar() { // Postal address pickers (and legacy pickers) don't support search, so just show // "HomeAsUp" button and title. if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL || mRequest.isLegacyCompatibilityMode()) { findViewById(R.id.search_view).setVisibility(View.GONE); final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowHomeEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayShowTitleEnabled(true); } return; } // If ActionBar is available, show SearchView on it. If not, show SearchView inside the // Activity's layout. final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { final View searchViewOnLayout = findViewById(R.id.search_view); if (searchViewOnLayout != null) { searchViewOnLayout.setVisibility(View.GONE); } final View searchViewContainer = LayoutInflater.from(actionBar.getThemedContext()) .inflate(R.layout.custom_action_bar, null); mSearchView = (SearchView) searchViewContainer.findViewById(R.id.search_view); // In order to make the SearchView look like "shown via search menu", we need to // manually setup its state. See also DialtactsActivity.java and ActionBarAdapter.java. mSearchView.setIconifiedByDefault(true); mSearchView.setQueryHint(getString(R.string.hint_findContacts)); mSearchView.setIconified(false); mSearchView.setOnQueryTextListener(this); mSearchView.setOnCloseListener(this); mSearchView.setOnQueryTextFocusChangeListener(this); actionBar.setCustomView(searchViewContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); actionBar.setDisplayShowCustomEnabled(true); actionBar.setDisplayShowHomeEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true); } else { mSearchView = (SearchView) findViewById(R.id.search_view); mSearchView.setQueryHint(getString(R.string.hint_findContacts)); mSearchView.setOnQueryTextListener(this); // This is a hack to prevent the search view from grabbing focus // at this point. If search view were visible, it would always grabs focus // because it is the first focusable widget in the window. mSearchView.setVisibility(View.INVISIBLE); mSearchView.postDelayed(new Runnable() { @Override public void run() { mSearchView.setVisibility(View.VISIBLE); } }, FOCUS_DELAY); } // Clear focus and suppress keyboard show-up. mSearchView.clearFocus(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // If we want "Create New Contact" button but there's no such a button in the layout, // try showing a menu for it. if (shouldShowCreateNewContactButton() && mCreateNewContactButton == null) { MenuInflater inflater = getSupportMenuInflater(); inflater.inflate(R.menu.contact_picker_options, menu); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: // Go back to previous screen, intending "cancel" setResult(RESULT_CANCELED); finish(); return true; case R.id.create_new_contact: { startCreateNewContactActivity(); return true; } } return super.onOptionsItemSelected(item); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_ACTION_CODE, mActionCode); } private void configureActivityTitle() { if (!TextUtils.isEmpty(mRequest.getActivityTitle())) { setTitle(mRequest.getActivityTitle()); return; } int actionCode = mRequest.getActionCode(); switch (actionCode) { case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: { setTitle(R.string.contactPickerActivityTitle); break; } case ContactsRequest.ACTION_PICK_CONTACT: { setTitle(R.string.contactPickerActivityTitle); break; } case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: { setTitle(R.string.contactPickerActivityTitle); break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: { setTitle(R.string.shortcutActivityTitle); break; } case ContactsRequest.ACTION_PICK_PHONE: { setTitle(R.string.contactPickerActivityTitle); break; } case ContactsRequest.ACTION_PICK_EMAIL: { setTitle(R.string.contactPickerActivityTitle); break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: { setTitle(R.string.callShortcutActivityTitle); break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: { setTitle(R.string.messageShortcutActivityTitle); break; } case ContactsRequest.ACTION_PICK_POSTAL: { setTitle(R.string.contactPickerActivityTitle); break; } } } /** * Creates the fragment based on the current request. */ public void configureListFragment() { switch (mActionCode) { case ContactsRequest.ACTION_INSERT_OR_EDIT_CONTACT: { ContactPickerFragment fragment = new ContactPickerFragment(); fragment.setEditMode(true); fragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); mListFragment = fragment; break; } case ContactsRequest.ACTION_PICK_CONTACT: { ContactPickerFragment fragment = new ContactPickerFragment(); fragment.setIncludeProfile(mRequest.shouldIncludeProfile()); mListFragment = fragment; break; } case ContactsRequest.ACTION_PICK_OR_CREATE_CONTACT: { ContactPickerFragment fragment = new ContactPickerFragment(); mListFragment = fragment; break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_CONTACT: { ContactPickerFragment fragment = new ContactPickerFragment(); fragment.setShortcutRequested(true); mListFragment = fragment; break; } case ContactsRequest.ACTION_PICK_PHONE: { PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment(); fragment.setUseCallableUri(true); mListFragment = fragment; break; } case ContactsRequest.ACTION_PICK_EMAIL: { mListFragment = new EmailAddressPickerFragment(); break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_CALL: { PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment(); fragment.setShortcutAction(Intent.ACTION_CALL); mListFragment = fragment; break; } case ContactsRequest.ACTION_CREATE_SHORTCUT_SMS: { PhoneNumberPickerFragment fragment = new PhoneNumberPickerFragment(); fragment.setShortcutAction(Intent.ACTION_SENDTO); mListFragment = fragment; break; } // case ContactsRequest.ACTION_PICK_POSTAL: { // PostalAddressPickerFragment fragment = new PostalAddressPickerFragment(); // mListFragment = fragment; // break; // } default: throw new IllegalStateException("Invalid action code: " + mActionCode); } mListFragment.setLegacyCompatibilityMode(mRequest.isLegacyCompatibilityMode()); mListFragment.setDirectoryResultLimit(DEFAULT_DIRECTORY_RESULT_LIMIT); getSupportFragmentManager().beginTransaction() .replace(R.id.list_container, mListFragment) .commitAllowingStateLoss(); } public void setupActionListener() { if (mListFragment instanceof ContactPickerFragment) { ((ContactPickerFragment) mListFragment).setOnContactPickerActionListener(new ContactPickerActionListener()); } else if (mListFragment instanceof PhoneNumberPickerFragment) { ((PhoneNumberPickerFragment) mListFragment).setOnPhoneNumberPickerActionListener( new PhoneNumberPickerActionListener()); } // else if (mListFragment instanceof PostalAddressPickerFragment) { // ((PostalAddressPickerFragment) mListFragment).setOnPostalAddressPickerActionListener( // new PostalAddressPickerActionListener()); // } else if (mListFragment instanceof EmailAddressPickerFragment) { ((EmailAddressPickerFragment) mListFragment).setOnEmailAddressPickerActionListener( new EmailAddressPickerActionListener()); } else { throw new IllegalStateException("Unsupported list fragment type: " + mListFragment); } } private final class ContactPickerActionListener implements OnContactPickerActionListener { @Override public void onCreateNewContactAction() { startCreateNewContactActivity(); } @Override public void onEditContactAction(Uri contactLookupUri) { Bundle extras = getIntent().getExtras(); if (launchAddToContactDialog(extras)) { // Show a confirmation dialog to add the value(s) to the existing contact. Intent intent = new Intent(ScContactSelectionActivity.this, ConfirmAddDetailActivity.class); intent.setData(contactLookupUri); if (extras != null) { // First remove name key if present because the dialog does not support name // editing. This is fine because the user wants to add information to an // existing contact, who should already have a name and we wouldn't want to // override the name. extras.remove(Insert.NAME); intent.putExtras(extras); } // Wait for the activity result because we want to keep the picker open (in case the // user cancels adding the info to a contact and wants to pick someone else). startActivityForResult(intent, SUBACTIVITY_ADD_TO_EXISTING_CONTACT); } else { // Otherwise launch the full contact editor. startActivityAndForwardResult(new Intent(Intent.ACTION_EDIT, contactLookupUri)); } } @Override public void onPickContactAction(Uri contactUri) { returnPickerResult(contactUri); } @Override public void onShortcutIntentCreated(Intent intent) { returnPickerResult(intent); } /** * Returns true if is a single email or single phone number provided in the {@link Intent} * extras bundle so that a pop-up confirmation dialog can be used to add the data to * a contact. Otherwise return false if there are other intent extras that require launching * the full contact editor. Ignore extras with the key {@link Insert.NAME} because names * are a special case and we typically don't want to replace the name of an existing * contact. */ private boolean launchAddToContactDialog(Bundle extras) { if (extras == null) { return false; } // Copy extras because the set may be modified in the next step Set<String> intentExtraKeys = Sets.newHashSet(); intentExtraKeys.addAll(extras.keySet()); // Ignore name key because this is an existing contact. if (intentExtraKeys.contains(Insert.NAME)) { intentExtraKeys.remove(Insert.NAME); } int numIntentExtraKeys = intentExtraKeys.size(); if (numIntentExtraKeys == 2) { boolean hasPhone = intentExtraKeys.contains(Insert.PHONE) && intentExtraKeys.contains(Insert.PHONE_TYPE); boolean hasEmail = intentExtraKeys.contains(Insert.EMAIL) && intentExtraKeys.contains(Insert.EMAIL_TYPE); return hasPhone || hasEmail; } else if (numIntentExtraKeys == 1) { return intentExtraKeys.contains(Insert.PHONE) || intentExtraKeys.contains(Insert.EMAIL); } // Having 0 or more than 2 intent extra keys means that we should launch // the full contact editor to properly handle the intent extras. return false; } } private final class PhoneNumberPickerActionListener implements OnPhoneNumberPickerActionListener { @Override public void onPickPhoneNumberAction(Uri dataUri) { returnPickerResult(dataUri); } @Override public void onShortcutIntentCreated(Intent intent) { returnPickerResult(intent); } public void onHomeInActionBarSelected() { ScContactSelectionActivity.this.onBackPressed(); } } // private final class PostalAddressPickerActionListener implements OnPostalAddressPickerActionListener { // @Override // public void onPickPostalAddressAction(Uri dataUri) { // returnPickerResult(dataUri); // } // } // private final class EmailAddressPickerActionListener implements OnEmailAddressPickerActionListener { @Override public void onPickEmailAddressAction(Uri dataUri) { returnPickerResult(dataUri); } } public void startActivityAndForwardResult(final Intent intent) { intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); // Forward extras to the new activity Bundle extras = getIntent().getExtras(); if (extras != null) { intent.putExtras(extras); } startActivity(intent); finish(); } @Override public boolean onQueryTextChange(String newText) { mListFragment.setQueryString(newText, true); return false; } @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onClose() { if (!TextUtils.isEmpty(mSearchView.getQuery())) { mSearchView.setQuery(null, true); } return true; } @Override public void onFocusChange(View view, boolean hasFocus) { switch (view.getId()) { case R.id.search_view: { if (hasFocus) { showInputMethod(mSearchView.findFocus()); } } } } public void returnPickerResult(Uri data) { Intent intent = new Intent(); intent.setData(data); returnPickerResult(intent); } public void returnPickerResult(Intent intent) { setResult(RESULT_OK, intent); finish(); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.new_contact: { startCreateNewContactActivity(); break; } } } private void startCreateNewContactActivity() { Intent intent = new Intent(Intent.ACTION_INSERT, RawContacts.CONTENT_URI); intent.putExtra(ScContactEditorActivity.INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, true); startActivityAndForwardResult(intent); } private void showInputMethod(View view) { final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { if (!imm.showSoftInput(view, 0)) { Log.w(TAG, "Failed to show soft input method."); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == SUBACTIVITY_ADD_TO_EXISTING_CONTACT) { if (resultCode == Activity.RESULT_OK) { if (data != null) { startActivity(data); } finish(); } } } }