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