DialtactsActivity.java revision 90374a77ce339f30a04fbb5d3c5faa0c84901c71
1/*
2 * Copyright (C) 2013 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.dialer;
18
19import android.animation.LayoutTransition;
20import android.app.ActionBar;
21import android.app.Activity;
22import android.app.Fragment;
23import android.app.FragmentManager;
24import android.app.FragmentTransaction;
25import android.content.ActivityNotFoundException;
26import android.content.Context;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.res.TypedArray;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.provider.ContactsContract.Contacts;
36import android.provider.ContactsContract.Intents;
37import android.speech.RecognizerIntent;
38import android.support.v4.view.ViewPager;
39import android.telephony.TelephonyManager;
40import android.text.Editable;
41import android.text.TextUtils;
42import android.text.TextWatcher;
43import android.util.Log;
44import android.view.DragEvent;
45import android.view.KeyEvent;
46import android.view.Menu;
47import android.view.MenuItem;
48import android.view.MotionEvent;
49import android.view.View;
50import android.view.View.OnDragListener;
51import android.view.animation.AccelerateInterpolator;
52import android.view.animation.Animation;
53import android.view.animation.Animation.AnimationListener;
54import android.view.animation.AnimationUtils;
55import android.view.animation.DecelerateInterpolator;
56import android.view.animation.Interpolator;
57import android.view.inputmethod.InputMethodManager;
58import android.widget.AbsListView.OnScrollListener;
59import android.widget.EditText;
60import android.widget.ImageButton;
61import android.widget.PopupMenu;
62import android.widget.RelativeLayout;
63import android.widget.Toast;
64
65import com.android.contacts.common.CallUtil;
66import com.android.contacts.common.activity.TransactionSafeActivity;
67import com.android.contacts.common.dialog.ClearFrequentsDialog;
68import com.android.contacts.common.interactions.ImportExportDialogFragment;
69import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
70import com.android.contacts.common.util.ViewUtil;
71import com.android.dialer.calllog.CallLogActivity;
72import com.android.dialer.database.DialerDatabaseHelper;
73import com.android.dialer.dialpad.DialpadFragment;
74import com.android.dialer.dialpad.SmartDialNameMatcher;
75import com.android.dialer.dialpad.SmartDialPrefix;
76import com.android.dialer.interactions.PhoneNumberInteraction;
77import com.android.dialer.list.DragDropController;
78import com.android.dialer.list.ListsFragment;
79import com.android.dialer.list.OnDragDropListener;
80import com.android.dialer.list.OnListFragmentScrolledListener;
81import com.android.dialer.list.SpeedDialFragment;
82import com.android.dialer.list.PhoneFavoriteSquareTileView;
83import com.android.dialer.list.RegularSearchFragment;
84import com.android.dialer.list.RemoveView;
85import com.android.dialer.list.SearchFragment;
86import com.android.dialer.list.SmartDialSearchFragment;
87import com.android.dialer.widget.SearchEditTextLayout;
88import com.android.dialerbind.DatabaseHelperManager;
89import com.android.internal.telephony.ITelephony;
90
91import java.util.ArrayList;
92import java.util.List;
93
94/**
95 * The dialer tab's title is 'phone', a more common name (see strings.xml).
96 */
97public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
98        DialpadFragment.OnDialpadQueryChangedListener,
99        OnListFragmentScrolledListener,
100        DialpadFragment.HostInterface,
101        ListsFragment.HostInterface,
102        SpeedDialFragment.HostInterface,
103        OnDragDropListener,
104        OnPhoneNumberPickerActionListener,
105        PopupMenu.OnMenuItemClickListener,
106        ViewPager.OnPageChangeListener {
107    private static final String TAG = "DialtactsActivity";
108
109    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
110
111    public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
112
113    /** Used to open Call Setting */
114    private static final String PHONE_PACKAGE = "com.android.phone";
115    private static final String CALL_SETTINGS_CLASS_NAME =
116            "com.android.phone.CallFeaturesSetting";
117    /** @see #getCallOrigin() */
118    private static final String CALL_ORIGIN_DIALTACTS =
119            "com.android.dialer.DialtactsActivity";
120
121    private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
122    private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
123    private static final String KEY_SEARCH_QUERY = "search_query";
124    private static final String KEY_FIRST_LAUNCH = "first_launch";
125
126    private static final String TAG_DIALPAD_FRAGMENT = "dialpad";
127    private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search";
128    private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial";
129    private static final String TAG_FAVORITES_FRAGMENT = "favorites";
130
131    /**
132     * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
133     */
134    private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
135
136    private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
137
138    private static final int ANIMATION_DURATION = 250;
139
140    private RelativeLayout parentLayout;
141
142    /**
143     * Fragment containing the dialpad that slides into view
144     */
145    private DialpadFragment mDialpadFragment;
146
147    /**
148     * Fragment for searching phone numbers using the alphanumeric keyboard.
149     */
150    private RegularSearchFragment mRegularSearchFragment;
151
152    /**
153     * Fragment for searching phone numbers using the dialpad.
154     */
155    private SmartDialSearchFragment mSmartDialSearchFragment;
156
157    /**
158     * Fragment containing the speed dial list, recents list, and all contacts list.
159     */
160    private ListsFragment mListsFragment;
161
162    private View mFloatingActionButtonContainer;
163    private ImageButton mFloatingActionButton;
164
165    private int mActionBarHeight;
166    private boolean mInDialpadSearch;
167    private boolean mInRegularSearch;
168    private boolean mClearSearchOnPause;
169    private boolean mIsDialpadShown;
170
171    /**
172     * The position of the currently selected tab in the attached {@link ListsFragment}.
173     */
174    private int mCurrentTabPosition = 0;
175
176    /**
177     * True if the dialpad is only temporarily showing due to being in call
178     */
179    private boolean mInCallDialpadUp;
180
181    /**
182     * True when this activity has been launched for the first time.
183     */
184    private boolean mFirstLaunch;
185
186    /**
187     * Search query to be applied to the SearchView in the ActionBar once
188     * onCreateOptionsMenu has been called.
189     */
190    private String mPendingSearchViewQuery;
191
192    private EditText mSearchView;
193    private View mSearchViewCloseButton;
194    private View mVoiceSearchButton;
195
196    /**
197     * View that contains the "Remove" dialog that shows up when the user long presses a contact.
198     * If the user releases a contact when hovering on top of this, the contact is unfavorited and
199     * removed from the speed dial list.
200     */
201    private View mRemoveViewContainer;
202
203    final Interpolator hideActionBarInterpolator = new AccelerateInterpolator(1.5f);
204    final Interpolator showActionBarInterpolator = new DecelerateInterpolator(1.5f);
205    private String mSearchQuery;
206
207    private DialerDatabaseHelper mDialerDatabaseHelper;
208    private DragDropController mDragDropController;
209
210    private class OptionsPopupMenu extends PopupMenu {
211        public OptionsPopupMenu(Context context, View anchor) {
212            super(context, anchor);
213        }
214
215        @Override
216        public void show() {
217            final Menu menu = getMenu();
218            final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
219            clearFrequents.setVisible(mListsFragment != null &&
220                    mListsFragment.getSpeedDialFragment() != null &&
221                    mListsFragment.getSpeedDialFragment().hasFrequents());
222            super.show();
223        }
224    }
225
226    /**
227     * Listener that listens to drag events and sends their x and y coordinates to a
228     * {@link DragDropController}.
229     */
230    private class LayoutOnDragListener implements OnDragListener {
231        @Override
232        public boolean onDrag(View v, DragEvent event) {
233            if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
234                mDragDropController.handleDragHovered(v, (int) event.getX(),
235                        (int) event.getY());
236            }
237            return true;
238        }
239    }
240
241    /**
242     * Listener used to send search queries to the phone search fragment.
243     */
244    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
245        @Override
246        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
247        }
248
249        @Override
250        public void onTextChanged(CharSequence s, int start, int before, int count) {
251            final String newText = s.toString();
252            if (newText.equals(mSearchQuery)) {
253                // If the query hasn't changed (perhaps due to activity being destroyed
254                // and restored, or user launching the same DIAL intent twice), then there is
255                // no need to do anything here.
256                return;
257            }
258            mSearchQuery = newText;
259            if (DEBUG) {
260                Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText);
261            }
262
263            // Show search fragment only when the query string is changed to non-empty text.
264            if (!TextUtils.isEmpty(newText)) {
265                // Call enterSearchUi only if we are switching search modes, or showing a search
266                // fragment for the first time.
267                final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) ||
268                        (!mIsDialpadShown && mInRegularSearch);
269                if (!sameSearchMode) {
270                    enterSearchUi(mIsDialpadShown, mSearchQuery);
271                }
272            }
273
274            if (mIsDialpadShown && mSmartDialSearchFragment != null) {
275                mSmartDialSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
276            } else if (mRegularSearchFragment != null) {
277                mRegularSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
278            }
279
280            if (TextUtils.isEmpty(newText)) {
281                mSearchViewCloseButton.setVisibility(View.GONE);
282                mVoiceSearchButton.setVisibility(View.VISIBLE);
283            } else {
284                mSearchViewCloseButton.setVisibility(View.VISIBLE);
285                mVoiceSearchButton.setVisibility(View.GONE);
286            }
287        }
288
289        @Override
290        public void afterTextChanged(Editable s) {
291        }
292    };
293
294
295    /**
296     * Open the search UI when the user touches the search text view.
297     */
298    private final View.OnTouchListener mSearchViewOnTouchListener = new View.OnTouchListener() {
299        @Override
300        public boolean onTouch(View v, MotionEvent event) {
301            if (!isInSearchUi()) {
302                enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString());
303            }
304            return false;
305        }
306    };
307
308    /**
309     * If the search term is empty and the user closes the soft keyboard, close the search UI.
310     */
311    private final View.OnKeyListener mSearchEditTextLayoutListener = new View.OnKeyListener() {
312        @Override
313        public boolean onKey(View v, int keyCode, KeyEvent event) {
314            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN &&
315                    TextUtils.isEmpty(mSearchView.getText().toString())) {
316                onBackPressed();
317            }
318            return false;
319        }
320    };
321
322    @Override
323    protected void onCreate(Bundle savedInstanceState) {
324        super.onCreate(savedInstanceState);
325        mFirstLaunch = true;
326
327        setContentView(R.layout.dialtacts_activity);
328        getWindow().setBackgroundDrawable(null);
329
330        final ActionBar actionBar = getActionBar();
331        actionBar.setCustomView(R.layout.search_edittext);
332        actionBar.setDisplayShowCustomEnabled(true);
333
334        SearchEditTextLayout actionBarView = (SearchEditTextLayout) actionBar.getCustomView();
335        actionBarView.setPreImeKeyListener(mSearchEditTextLayoutListener);
336
337        mSearchView = (EditText) actionBarView.findViewById(R.id.search_view);
338        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
339        mSearchView.setOnTouchListener(mSearchViewOnTouchListener);
340
341        mSearchViewCloseButton = actionBarView.findViewById(R.id.search_close_button);
342        mSearchViewCloseButton.setOnClickListener(this);
343
344        ImageButton optionsMenuButton =
345                (ImageButton) actionBarView.findViewById(R.id.dialtacts_options_menu_button);
346        optionsMenuButton.setOnClickListener(this);
347        final OptionsPopupMenu optionsMenu = buildOptionsMenu(optionsMenuButton);
348        optionsMenuButton.setOnTouchListener(optionsMenu.getDragToOpenListener());
349
350        final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
351                new int[] { android.R.attr.actionBarSize });
352        mActionBarHeight = (int) styledAttributes.getDimension(0, 0);
353        styledAttributes.recycle();
354
355        // Add the favorites fragment, and the dialpad fragment, but only if savedInstanceState
356        // is null. Otherwise the fragment manager takes care of recreating these fragments.
357        if (savedInstanceState == null) {
358            getFragmentManager().beginTransaction()
359                    .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT)
360                    .add(R.id.dialtacts_container, new DialpadFragment(), TAG_DIALPAD_FRAGMENT)
361                    .commit();
362        } else {
363            mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY);
364            mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI);
365            mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI);
366            mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH);
367        }
368
369        parentLayout = (RelativeLayout) findViewById(R.id.dialtacts_mainlayout);
370        parentLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
371        parentLayout.setOnDragListener(new LayoutOnDragListener());
372
373        mFloatingActionButtonContainer = findViewById(R.id.floating_action_button_container);
374        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, getResources());
375
376        mFloatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
377        mFloatingActionButton.setOnClickListener(this);
378
379        mRemoveViewContainer = findViewById(R.id.remove_view_container);
380
381        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
382        SmartDialPrefix.initializeNanpSettings(this);
383    }
384
385    @Override
386    protected void onResume() {
387        super.onResume();
388        if (mFirstLaunch) {
389            displayFragment(getIntent());
390        } else if (!phoneIsInUse() && mInCallDialpadUp) {
391            hideDialpadFragment(false, true);
392            mInCallDialpadUp = false;
393        }
394        mFirstLaunch = false;
395        prepareVoiceSearchButton();
396        mDialerDatabaseHelper.startSmartDialUpdateThread();
397    }
398
399    @Override
400    protected void onPause() {
401        if (mClearSearchOnPause) {
402            hideDialpadAndSearchUi();
403            mClearSearchOnPause = false;
404        }
405        super.onPause();
406    }
407
408    @Override
409    protected void onSaveInstanceState(Bundle outState) {
410        super.onSaveInstanceState(outState);
411        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
412        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
413        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
414        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
415    }
416
417    @Override
418    public void onAttachFragment(Fragment fragment) {
419        if (fragment instanceof DialpadFragment) {
420            mDialpadFragment = (DialpadFragment) fragment;
421            final FragmentTransaction transaction = getFragmentManager().beginTransaction();
422            transaction.hide(mDialpadFragment);
423            transaction.commit();
424        } else if (fragment instanceof SmartDialSearchFragment) {
425            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
426            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
427        } else if (fragment instanceof SearchFragment) {
428            mRegularSearchFragment = (RegularSearchFragment) fragment;
429            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
430        } else if (fragment instanceof ListsFragment) {
431            mListsFragment = (ListsFragment) fragment;
432            mListsFragment.addOnPageChangeListener(this);
433        }
434    }
435
436    protected void handleMenuSettings() {
437        openTelephonySetting(this);
438    }
439
440    public static void openTelephonySetting(Activity activity) {
441        final Intent settingsIntent = getCallSettingsIntent();
442        activity.startActivity(settingsIntent);
443    }
444
445    @Override
446    public void onClick(View view) {
447        switch (view.getId()) {
448            case R.id.floating_action_button:
449                if (!mIsDialpadShown) {
450                    mInCallDialpadUp = false;
451                    showDialpadFragment(true);
452                } else {
453                    // Dial button was pressed; tell the Dialpad fragment
454                    mDialpadFragment.dialButtonPressed();
455                }
456                break;
457            case R.id.search_close_button:
458                // Clear the search field
459                if (!TextUtils.isEmpty(mSearchView.getText())) {
460                    mDialpadFragment.clearDialpad();
461                    mSearchView.setText(null);
462                }
463                break;
464            case R.id.voice_search_button:
465                try {
466                    startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
467                            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
468                } catch (ActivityNotFoundException e) {
469                    Toast.makeText(DialtactsActivity.this, R.string.voice_search_not_available,
470                            Toast.LENGTH_SHORT).show();
471                }
472                break;
473            case R.id.dialtacts_options_menu_button:
474                buildOptionsMenu(view).show();
475                break;
476            default: {
477                Log.wtf(TAG, "Unexpected onClick event from " + view);
478                break;
479            }
480        }
481    }
482
483    @Override
484    public boolean onMenuItemClick(MenuItem item) {
485        switch (item.getItemId()) {
486            case R.id.menu_history:
487                showCallHistory();
488                break;
489            case R.id.menu_add_contact:
490                try {
491                    startActivity(new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
492                } catch (ActivityNotFoundException e) {
493                    Toast toast = Toast.makeText(this,
494                            R.string.add_contact_not_available,
495                            Toast.LENGTH_SHORT);
496                    toast.show();
497                }
498                break;
499            case R.id.menu_import_export:
500                // We hard-code the "contactsAreAvailable" argument because doing it properly would
501                // involve querying a {@link ProviderStatusLoader}, which we don't want to do right
502                // now in Dialtacts for (potential) performance reasons. Compare with how it is
503                // done in {@link PeopleActivity}.
504                ImportExportDialogFragment.show(getFragmentManager(), true,
505                        DialtactsActivity.class);
506                return true;
507            case R.id.menu_clear_frequents:
508                ClearFrequentsDialog.show(getFragmentManager());
509                return true;
510            case R.id.menu_call_settings:
511                handleMenuSettings();
512                return true;
513        }
514        return false;
515    }
516
517    @Override
518    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
519        if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
520            if (resultCode == RESULT_OK) {
521                final ArrayList<String> matches = data.getStringArrayListExtra(
522                        RecognizerIntent.EXTRA_RESULTS);
523                if (matches.size() > 0) {
524                    final String match = matches.get(0);
525                    mSearchView.setText(match);
526                } else {
527                    Log.e(TAG, "Voice search - nothing heard");
528                }
529            } else {
530                Log.e(TAG, "Voice search failed");
531            }
532        }
533        super.onActivityResult(requestCode, resultCode, data);
534    }
535
536    /**
537     * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual
538     * updates are handled by a callback which is invoked after the dialpad fragment is shown.
539     * @see #onDialpadShown
540     */
541    private void showDialpadFragment(boolean animate) {
542        if (mIsDialpadShown) {
543            return;
544        }
545        mIsDialpadShown = true;
546        mDialpadFragment.setAnimate(animate);
547
548        final FragmentTransaction ft = getFragmentManager().beginTransaction();
549        ft.show(mDialpadFragment);
550        ft.commit();
551
552        if (!isInSearchUi()) {
553            enterSearchUi(true /* isSmartDial */, mSearchQuery);
554        }
555    }
556
557    /**
558     * Callback from child DialpadFragment when the dialpad is shown.
559     */
560    public void onDialpadShown() {
561        updateFloatingActionButton();
562        if (mDialpadFragment.getAnimate()) {
563            Animation slideIn = AnimationUtils.loadAnimation(this, R.anim.slide_in);
564            mDialpadFragment.getView().startAnimation(slideIn);
565        } else {
566            mDialpadFragment.setYFraction(0);
567        }
568
569        updateSearchFragmentPosition();
570        getActionBar().hide();
571    }
572
573    /**
574     * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in
575     * a callback after the hide animation ends.
576     * @see #commitDialpadFragmentHide
577     */
578    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
579        if (mDialpadFragment == null) {
580            return;
581        }
582        if (clearDialpad) {
583            mDialpadFragment.clearDialpad();
584        }
585        if (!mIsDialpadShown) {
586            return;
587        }
588        mIsDialpadShown = false;
589        mDialpadFragment.setAnimate(animate);
590
591        updateFloatingActionButton();
592        if (animate) {
593            Animation slideOut = AnimationUtils.loadAnimation(this, R.anim.slide_out);
594            slideOut.setAnimationListener(new ActivityAnimationListener() {
595                @Override
596                public void onAnimationEnd(Animation animation) {
597                    commitDialpadFragmentHide();
598                }
599            });
600            mDialpadFragment.getView().startAnimation(slideOut);
601        } else {
602            commitDialpadFragmentHide();
603        }
604
605        updateSearchFragmentPosition();
606        mListsFragment.maybeShowActionBar();
607    }
608
609    /**
610     * Finishes hiding the dialpad fragment after any animations are completed.
611     */
612    private void commitDialpadFragmentHide() {
613        final FragmentTransaction ft = getFragmentManager().beginTransaction();
614        ft.hide(mDialpadFragment);
615        ft.commit();
616    }
617
618    private void updateSearchFragmentPosition() {
619        int translationValue = mIsDialpadShown ?  -mActionBarHeight : 0;
620        SearchFragment fragment = null;
621        if (mInDialpadSearch) {
622            fragment = mSmartDialSearchFragment;
623        } else if (mInRegularSearch) {
624            fragment = mRegularSearchFragment;
625        }
626        if (fragment != null && fragment.isVisible()) {
627            fragment.getView().animate().translationY(translationValue)
628                    .setInterpolator(hideActionBarInterpolator).setDuration(ANIMATION_DURATION);
629        }
630    }
631
632    private boolean isInSearchUi() {
633        return mInDialpadSearch || mInRegularSearch;
634    }
635
636    private void setNotInSearchUi() {
637        mInDialpadSearch = false;
638        mInRegularSearch = false;
639    }
640
641    private void hideDialpadAndSearchUi() {
642        mSearchView.setText(null);
643        hideDialpadFragment(false, true);
644    }
645
646    private void hideInputMethod(View view) {
647        final InputMethodManager imm = (InputMethodManager) getSystemService(
648                Context.INPUT_METHOD_SERVICE);
649        if (imm != null && view != null) {
650            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
651        }
652    }
653
654    private void prepareVoiceSearchButton() {
655        mVoiceSearchButton = getActionBar().getCustomView().findViewById(R.id.voice_search_button);
656        final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
657        if (canIntentBeHandled(voiceIntent)) {
658            mVoiceSearchButton.setVisibility(View.VISIBLE);
659            mVoiceSearchButton.setOnClickListener(this);
660        } else {
661            mVoiceSearchButton.setVisibility(View.GONE);
662        }
663    }
664
665    private OptionsPopupMenu buildOptionsMenu(View invoker) {
666        final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker);
667        popupMenu.inflate(R.menu.dialtacts_options);
668        popupMenu.setOnMenuItemClickListener(this);
669        return popupMenu;
670    }
671
672    @Override
673    public boolean onCreateOptionsMenu(Menu menu) {
674        if (mPendingSearchViewQuery != null) {
675            mSearchView.setText(mPendingSearchViewQuery);
676            mPendingSearchViewQuery = null;
677        }
678        return false;
679    }
680
681    /**
682     * Returns true if the intent is due to hitting the green send key (hardware call button:
683     * KEYCODE_CALL) while in a call.
684     *
685     * @param intent the intent that launched this activity
686     * @return true if the intent is due to hitting the green send key while in a call
687     */
688    private boolean isSendKeyWhileInCall(Intent intent) {
689        // If there is a call in progress and the user launched the dialer by hitting the call
690        // button, go straight to the in-call screen.
691        final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
692
693        try {
694            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
695            if (callKey && phone != null && phone.showCallScreen()) {
696                return true;
697            }
698        } catch (RemoteException e) {
699            Log.e(TAG, "Failed to handle send while in call", e);
700        }
701
702        return false;
703    }
704
705    /**
706     * Sets the current tab based on the intent's request type
707     *
708     * @param intent Intent that contains information about which tab should be selected
709     */
710    private void displayFragment(Intent intent) {
711        // If we got here by hitting send and we're in call forward along to the in-call activity
712        if (isSendKeyWhileInCall(intent)) {
713            finish();
714            return;
715        }
716
717        if (mDialpadFragment != null) {
718            final boolean phoneIsInUse = phoneIsInUse();
719            if (phoneIsInUse || isDialIntent(intent)) {
720                mDialpadFragment.setStartedFromNewIntent(true);
721                if (phoneIsInUse && !mDialpadFragment.isVisible()) {
722                    mInCallDialpadUp = true;
723                }
724                showDialpadFragment(false);
725            }
726        }
727    }
728
729    @Override
730    public void onNewIntent(Intent newIntent) {
731        setIntent(newIntent);
732        displayFragment(newIntent);
733
734        invalidateOptionsMenu();
735    }
736
737    /** Returns true if the given intent contains a phone number to populate the dialer with */
738    private boolean isDialIntent(Intent intent) {
739        final String action = intent.getAction();
740        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
741            return true;
742        }
743        if (Intent.ACTION_VIEW.equals(action)) {
744            final Uri data = intent.getData();
745            if (data != null && CallUtil.SCHEME_TEL.equals(data.getScheme())) {
746                return true;
747            }
748        }
749        return false;
750    }
751
752    /**
753     * Returns an appropriate call origin for this Activity. May return null when no call origin
754     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
755     * for remembering the tab in which the user made a phone call, so the external app's DIAL
756     * request should not be counted.)
757     */
758    public String getCallOrigin() {
759        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
760    }
761
762    /**
763     * Shows the search fragment
764     */
765    private void enterSearchUi(boolean smartDialSearch, String query) {
766        if (getFragmentManager().isDestroyed()) {
767            // Weird race condition where fragment is doing work after the activity is destroyed
768            // due to talkback being on (b/10209937). Just return since we can't do any
769            // constructive here.
770            return;
771        }
772
773        if (DEBUG) {
774            Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
775        }
776
777        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
778        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
779            transaction.remove(mSmartDialSearchFragment);
780        } else if (mInRegularSearch && mRegularSearchFragment != null) {
781            transaction.remove(mRegularSearchFragment);
782        }
783
784        final String tag;
785        if (smartDialSearch) {
786            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
787        } else {
788            tag = TAG_REGULAR_SEARCH_FRAGMENT;
789        }
790        mInDialpadSearch = smartDialSearch;
791        mInRegularSearch = !smartDialSearch;
792
793        SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
794        transaction.setCustomAnimations(android.R.animator.fade_in, 0);
795        if (fragment == null) {
796            if (smartDialSearch) {
797                fragment = new SmartDialSearchFragment();
798            } else {
799                fragment = new RegularSearchFragment();
800            }
801            transaction.add(R.id.dialtacts_frame, fragment, tag);
802        } else {
803            transaction.show(fragment);
804        }
805
806        // DialtactsActivity will provide the options menu
807        fragment.setHasOptionsMenu(false);
808        fragment.setShowEmptyListForNullQuery(true);
809        fragment.setQueryString(query, false /* delaySelection */);
810        transaction.commit();
811
812        mListsFragment.getView().animate().alpha(0).withLayer();
813    }
814
815    /**
816     * Hides the search fragment
817     */
818    private void exitSearchUi() {
819        // See related bug in enterSearchUI();
820        if (getFragmentManager().isDestroyed()) {
821            return;
822        }
823
824        mSearchView.setText(null);
825        mDialpadFragment.clearDialpad();
826        setNotInSearchUi();
827
828        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
829        transaction.setCustomAnimations(0, android.R.animator.fade_out);
830
831        if (mSmartDialSearchFragment != null) {
832            transaction.remove(mSmartDialSearchFragment);
833        }
834        if (mRegularSearchFragment != null) {
835            transaction.remove(mRegularSearchFragment);
836        }
837        transaction.commit();
838
839        mListsFragment.getView().animate().alpha(1).withLayer();
840    }
841
842    /** Returns an Intent to launch Call Settings screen */
843    public static Intent getCallSettingsIntent() {
844        final Intent intent = new Intent(Intent.ACTION_MAIN);
845        intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
846        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
847        return intent;
848    }
849
850    @Override
851    public void onBackPressed() {
852        if (mIsDialpadShown) {
853            if (TextUtils.isEmpty(mSearchQuery)) {
854                exitSearchUi();
855            }
856            hideDialpadFragment(true, false);
857        } else if (isInSearchUi()) {
858            exitSearchUi();
859        } else {
860            super.onBackPressed();
861        }
862    }
863
864    @Override
865    public void onDialpadQueryChanged(String query) {
866        if (mSmartDialSearchFragment != null) {
867            mSmartDialSearchFragment.setAddToContactNumber(query);
868        }
869        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
870                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
871
872        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
873            if (DEBUG) {
874                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
875            }
876            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
877                // This callback can happen if the dialpad fragment is recreated because of
878                // activity destruction. In that case, don't update the search view because
879                // that would bring the user back to the search fragment regardless of the
880                // previous state of the application. Instead, just return here and let the
881                // fragment manager correctly figure out whatever fragment was last displayed.
882                if (!TextUtils.isEmpty(normalizedQuery)) {
883                    mPendingSearchViewQuery = normalizedQuery;
884                }
885                return;
886            }
887            mSearchView.setText(normalizedQuery);
888        }
889    }
890
891    @Override
892    public void onListFragmentScrollStateChange(int scrollState) {
893        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
894            hideDialpadFragment(true, false);
895            hideInputMethod(getCurrentFocus());
896        }
897    }
898
899    @Override
900    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
901                                     int totalItemCount) {
902        // TODO: No-op for now. This should eventually show/hide the actionBar based on
903        // interactions with the ListsFragments.
904    }
905
906    @Override
907    public void setFloatingActionButtonVisible(boolean visible) {
908        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
909    }
910
911    private boolean phoneIsInUse() {
912        final TelephonyManager tm = (TelephonyManager) getSystemService(
913                Context.TELEPHONY_SERVICE);
914        return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
915    }
916
917    public static Intent getAddNumberToContactIntent(CharSequence text) {
918        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
919        intent.putExtra(Intents.Insert.PHONE, text);
920        intent.setType(Contacts.CONTENT_ITEM_TYPE);
921        return intent;
922    }
923
924    private boolean canIntentBeHandled(Intent intent) {
925        final PackageManager packageManager = getPackageManager();
926        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
927                PackageManager.MATCH_DEFAULT_ONLY);
928        return resolveInfo != null && resolveInfo.size() > 0;
929    }
930
931    @Override
932    public void showCallHistory() {
933        // Use explicit CallLogActivity intent instead of ACTION_VIEW +
934        // CONTENT_TYPE, so that we always open our call log from our dialer
935        final Intent intent = new Intent(this, CallLogActivity.class);
936        startActivity(intent);
937    }
938
939    /**
940     * Called when the user has long-pressed a contact tile to start a drag operation.
941     */
942    @Override
943    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
944        getActionBar().hide();
945        mRemoveViewContainer.setVisibility(View.VISIBLE);
946    }
947
948    @Override
949    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
950    }
951
952    /**
953     * Called when the user has released a contact tile after long-pressing it.
954     */
955    @Override
956    public void onDragFinished(int x, int y) {
957        getActionBar().show();
958        mRemoveViewContainer.setVisibility(View.GONE);
959    }
960
961    @Override
962    public void onDroppedOnRemove() {}
963
964    /**
965     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
966     * once it has been attached to the activity.
967     */
968    @Override
969    public void setDragDropController(DragDropController dragController) {
970        mDragDropController = dragController;
971        ((RemoveView) findViewById(R.id.remove_view))
972                .setDragDropController(dragController);
973    }
974
975    @Override
976    public void onPickPhoneNumberAction(Uri dataUri) {
977        // Specify call-origin so that users will see the previous tab instead of
978        // CallLog screen (search UI will be automatically exited).
979        PhoneNumberInteraction.startInteractionForPhoneCall(
980                DialtactsActivity.this, dataUri, getCallOrigin());
981        mClearSearchOnPause = true;
982    }
983
984    @Override
985    public void onCallNumberDirectly(String phoneNumber) {
986        Intent intent = CallUtil.getCallIntent(phoneNumber, getCallOrigin());
987        startActivity(intent);
988        mClearSearchOnPause = true;
989    }
990
991    @Override
992    public void onShortcutIntentCreated(Intent intent) {
993        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
994    }
995
996    @Override
997    public void onHomeInActionBarSelected() {
998        exitSearchUi();
999    }
1000
1001    public int getActionBarHeight() {
1002        return mActionBarHeight;
1003    }
1004
1005    @Override
1006    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
1007
1008    }
1009
1010    @Override
1011    public void onPageSelected(int position) {
1012        mCurrentTabPosition = position;
1013        // If the dialpad is showing, the floating action button should always be middle aligned.
1014        if (!mIsDialpadShown) {
1015            alignFloatingActionButtonByTab(mCurrentTabPosition);
1016        }
1017    }
1018
1019    @Override
1020    public void onPageScrollStateChanged(int state) {
1021    }
1022
1023    private void updateFloatingActionButton() {
1024        if (mIsDialpadShown) {
1025            mFloatingActionButton.setImageResource(R.drawable.fab_ic_call);
1026            mFloatingActionButton.setContentDescription(
1027                    getResources().getString(R.string.description_dial_button));
1028            alignFloatingActionButtonMiddle();
1029        } else {
1030            mFloatingActionButton.setImageResource(R.drawable.fab_ic_dial);
1031            mFloatingActionButton.setContentDescription(
1032                    getResources().getString(R.string.action_menu_dialpad_button));
1033            alignFloatingActionButtonByTab(mCurrentTabPosition);
1034        }
1035    }
1036
1037    private void alignFloatingActionButtonByTab(int position) {
1038        if (position == ListsFragment.TAB_INDEX_SPEED_DIAL) {
1039            alignFloatingActionButtonMiddle();
1040        } else {
1041            alignFloatingActionButtonRight();
1042        }
1043    }
1044
1045    private void alignFloatingActionButtonRight() {
1046        final RelativeLayout.LayoutParams params =
1047                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1048        params.removeRule(RelativeLayout.CENTER_HORIZONTAL);
1049        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1050        mFloatingActionButtonContainer.setLayoutParams(params);
1051    }
1052
1053    private void alignFloatingActionButtonMiddle() {
1054        final RelativeLayout.LayoutParams params =
1055                (RelativeLayout.LayoutParams) mFloatingActionButtonContainer.getLayoutParams();
1056        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT);
1057        params.addRule(RelativeLayout.CENTER_HORIZONTAL);
1058        mFloatingActionButtonContainer.setLayoutParams(params);
1059    }
1060
1061    /**
1062     * Convenience class which implements AnimationListener interface as null-op methods.
1063     */
1064    private class ActivityAnimationListener implements AnimationListener {
1065        @Override
1066        public void onAnimationStart(Animation animation) {
1067        }
1068
1069        @Override
1070        public void onAnimationEnd(Animation animation) {
1071        }
1072
1073        @Override
1074        public void onAnimationRepeat(Animation animation) {
1075        }
1076    }
1077}
1078