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.app.ActionBar;
20import android.app.Fragment;
21import android.app.FragmentTransaction;
22import android.content.ActivityNotFoundException;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.net.Uri;
30import android.os.Bundle;
31import android.provider.ContactsContract.Contacts;
32import android.provider.ContactsContract.Intents;
33import android.speech.RecognizerIntent;
34import android.support.v4.view.ViewPager;
35import android.telecom.PhoneAccount;
36import android.telecom.TelecomManager;
37import android.telephony.TelephonyManager;
38import android.text.Editable;
39import android.text.TextUtils;
40import android.text.TextWatcher;
41import android.util.Log;
42import android.view.DragEvent;
43import android.view.Gravity;
44import android.view.KeyEvent;
45import android.view.Menu;
46import android.view.MenuItem;
47import android.view.MotionEvent;
48import android.view.View;
49import android.view.View.OnDragListener;
50import android.view.View.OnTouchListener;
51import android.view.ViewTreeObserver;
52import android.view.animation.Animation;
53import android.view.animation.AnimationUtils;
54import android.widget.AbsListView.OnScrollListener;
55import android.widget.EditText;
56import android.widget.FrameLayout;
57import android.widget.ImageButton;
58import android.widget.PopupMenu;
59import android.widget.Toast;
60
61import com.android.contacts.common.CallUtil;
62import com.android.contacts.common.dialog.ClearFrequentsDialog;
63import com.android.contacts.common.interactions.ImportExportDialogFragment;
64import com.android.contacts.common.interactions.TouchPointManager;
65import com.android.contacts.common.list.OnPhoneNumberPickerActionListener;
66import com.android.contacts.common.widget.FloatingActionButtonController;
67import com.android.dialer.activity.TransactionSafeActivity;
68import com.android.dialer.calllog.CallLogActivity;
69import com.android.dialer.database.DialerDatabaseHelper;
70import com.android.dialer.dialpad.DialpadFragment;
71import com.android.dialer.dialpad.SmartDialNameMatcher;
72import com.android.dialer.dialpad.SmartDialPrefix;
73import com.android.dialer.interactions.PhoneNumberInteraction;
74import com.android.dialer.list.DragDropController;
75import com.android.dialer.list.ListsFragment;
76import com.android.dialer.list.OnDragDropListener;
77import com.android.dialer.list.OnListFragmentScrolledListener;
78import com.android.dialer.list.PhoneFavoriteSquareTileView;
79import com.android.dialer.list.RegularSearchFragment;
80import com.android.dialer.list.SearchFragment;
81import com.android.dialer.list.SmartDialSearchFragment;
82import com.android.dialer.list.SpeedDialFragment;
83import com.android.dialer.settings.DialerSettingsActivity;
84import com.android.dialer.util.DialerUtils;
85import com.android.dialer.widget.ActionBarController;
86import com.android.dialer.widget.SearchEditTextLayout;
87import com.android.dialer.widget.SearchEditTextLayout.OnBackButtonClickedListener;
88import com.android.dialerbind.DatabaseHelperManager;
89import com.android.incallui.CallCardFragment;
90import com.android.phone.common.animation.AnimUtils;
91import com.android.phone.common.animation.AnimationListenerAdapter;
92
93import java.util.ArrayList;
94import java.util.List;
95
96/**
97 * The dialer tab's title is 'phone', a more common name (see strings.xml).
98 */
99public class DialtactsActivity extends TransactionSafeActivity implements View.OnClickListener,
100        DialpadFragment.OnDialpadQueryChangedListener,
101        OnListFragmentScrolledListener,
102        ListsFragment.HostInterface,
103        SpeedDialFragment.HostInterface,
104        SearchFragment.HostInterface,
105        OnDragDropListener,
106        OnPhoneNumberPickerActionListener,
107        PopupMenu.OnMenuItemClickListener,
108        ViewPager.OnPageChangeListener,
109        ActionBarController.ActivityUi {
110    private static final String TAG = "DialtactsActivity";
111
112    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
113
114    public static final String SHARED_PREFS_NAME = "com.android.dialer_preferences";
115
116    /** @see #getCallOrigin() */
117    private static final String CALL_ORIGIN_DIALTACTS =
118            "com.android.dialer.DialtactsActivity";
119
120    private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui";
121    private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui";
122    private static final String KEY_SEARCH_QUERY = "search_query";
123    private static final String KEY_FIRST_LAUNCH = "first_launch";
124    private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown";
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 FrameLayout mParentLayout;
139
140    /**
141     * Fragment containing the dialpad that slides into view
142     */
143    private DialpadFragment mDialpadFragment;
144
145    /**
146     * Fragment for searching phone numbers using the alphanumeric keyboard.
147     */
148    private RegularSearchFragment mRegularSearchFragment;
149
150    /**
151     * Fragment for searching phone numbers using the dialpad.
152     */
153    private SmartDialSearchFragment mSmartDialSearchFragment;
154
155    /**
156     * Animation that slides in.
157     */
158    private Animation mSlideIn;
159
160    /**
161     * Animation that slides out.
162     */
163    private Animation mSlideOut;
164
165    /**
166     * Listener for after slide out animation completes on dialer fragment.
167     */
168    AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() {
169        @Override
170        public void onAnimationEnd(Animation animation) {
171            commitDialpadFragmentHide();
172        }
173    };
174
175    /**
176     * Fragment containing the speed dial list, recents list, and all contacts list.
177     */
178    private ListsFragment mListsFragment;
179
180    private boolean mInDialpadSearch;
181    private boolean mInRegularSearch;
182    private boolean mClearSearchOnPause;
183    private boolean mIsDialpadShown;
184    private boolean mShowDialpadOnResume;
185
186    /**
187     * Whether or not the device is in landscape orientation.
188     */
189    private boolean mIsLandscape;
190
191    /**
192     * The position of the currently selected tab in the attached {@link ListsFragment}.
193     */
194    private int mCurrentTabPosition = 0;
195
196    /**
197     * True if the dialpad is only temporarily showing due to being in call
198     */
199    private boolean mInCallDialpadUp;
200
201    /**
202     * True when this activity has been launched for the first time.
203     */
204    private boolean mFirstLaunch;
205
206    /**
207     * Search query to be applied to the SearchView in the ActionBar once
208     * onCreateOptionsMenu has been called.
209     */
210    private String mPendingSearchViewQuery;
211
212    private PopupMenu mOverflowMenu;
213    private EditText mSearchView;
214    private View mVoiceSearchButton;
215
216    private String mSearchQuery;
217
218    private DialerDatabaseHelper mDialerDatabaseHelper;
219    private DragDropController mDragDropController;
220    private ActionBarController mActionBarController;
221
222    private FloatingActionButtonController mFloatingActionButtonController;
223
224    private int mActionBarHeight;
225
226    private class OptionsPopupMenu extends PopupMenu {
227        public OptionsPopupMenu(Context context, View anchor) {
228            super(context, anchor, Gravity.END);
229        }
230
231        @Override
232        public void show() {
233            final Menu menu = getMenu();
234            final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents);
235            clearFrequents.setVisible(mListsFragment != null &&
236                    mListsFragment.getSpeedDialFragment() != null &&
237                    mListsFragment.getSpeedDialFragment().hasFrequents());
238            super.show();
239        }
240    }
241
242    /**
243     * Listener that listens to drag events and sends their x and y coordinates to a
244     * {@link DragDropController}.
245     */
246    private class LayoutOnDragListener implements OnDragListener {
247        @Override
248        public boolean onDrag(View v, DragEvent event) {
249            if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
250                mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY());
251            }
252            return true;
253        }
254    }
255
256    /**
257     * Listener used to send search queries to the phone search fragment.
258     */
259    private final TextWatcher mPhoneSearchQueryTextListener = new TextWatcher() {
260        @Override
261        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
262        }
263
264        @Override
265        public void onTextChanged(CharSequence s, int start, int before, int count) {
266            final String newText = s.toString();
267            if (newText.equals(mSearchQuery)) {
268                // If the query hasn't changed (perhaps due to activity being destroyed
269                // and restored, or user launching the same DIAL intent twice), then there is
270                // no need to do anything here.
271                return;
272            }
273            if (DEBUG) {
274                Log.d(TAG, "onTextChange for mSearchView called with new query: " + newText);
275                Log.d(TAG, "Previous Query: " + mSearchQuery);
276            }
277            mSearchQuery = newText;
278
279            // Show search fragment only when the query string is changed to non-empty text.
280            if (!TextUtils.isEmpty(newText)) {
281                // Call enterSearchUi only if we are switching search modes, or showing a search
282                // fragment for the first time.
283                final boolean sameSearchMode = (mIsDialpadShown && mInDialpadSearch) ||
284                        (!mIsDialpadShown && mInRegularSearch);
285                if (!sameSearchMode) {
286                    enterSearchUi(mIsDialpadShown, mSearchQuery);
287                }
288            }
289
290            if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
291                mSmartDialSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
292            } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
293                mRegularSearchFragment.setQueryString(mSearchQuery, false /* delaySelection */);
294            }
295        }
296
297        @Override
298        public void afterTextChanged(Editable s) {
299        }
300    };
301
302
303    /**
304     * Open the search UI when the user clicks on the search box.
305     */
306    private final View.OnClickListener mSearchViewOnClickListener = new View.OnClickListener() {
307        @Override
308        public void onClick(View v) {
309            if (!isInSearchUi()) {
310                mActionBarController.onSearchBoxTapped();
311                enterSearchUi(false /* smartDialSearch */, mSearchView.getText().toString());
312            }
313        }
314    };
315
316    /**
317     * If the search term is empty and the user closes the soft keyboard, close the search UI.
318     */
319    private final View.OnKeyListener mSearchEditTextLayoutListener = new View.OnKeyListener() {
320        @Override
321        public boolean onKey(View v, int keyCode, KeyEvent event) {
322            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN &&
323                    TextUtils.isEmpty(mSearchView.getText().toString())) {
324                maybeExitSearchUi();
325            }
326            return false;
327        }
328    };
329
330    @Override
331    public boolean dispatchTouchEvent(MotionEvent ev) {
332        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
333            TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
334        }
335        return super.dispatchTouchEvent(ev);
336
337    }
338
339    @Override
340    protected void onCreate(Bundle savedInstanceState) {
341        super.onCreate(savedInstanceState);
342        mFirstLaunch = true;
343
344        final Resources resources = getResources();
345        mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large);
346
347        setContentView(R.layout.dialtacts_activity);
348        getWindow().setBackgroundDrawable(null);
349
350        final ActionBar actionBar = getActionBar();
351        actionBar.setCustomView(R.layout.search_edittext);
352        actionBar.setDisplayShowCustomEnabled(true);
353        actionBar.setBackgroundDrawable(null);
354
355        mActionBarController = new ActionBarController(this,
356                (SearchEditTextLayout) actionBar.getCustomView());
357
358        SearchEditTextLayout searchEditTextLayout =
359                (SearchEditTextLayout) actionBar.getCustomView();
360        searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener);
361
362        mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view);
363        mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener);
364        mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button);
365        searchEditTextLayout.findViewById(R.id.search_magnifying_glass)
366                .setOnClickListener(mSearchViewOnClickListener);
367        searchEditTextLayout.findViewById(R.id.search_box_start_search)
368                .setOnClickListener(mSearchViewOnClickListener);
369        searchEditTextLayout.setOnBackButtonClickedListener(new OnBackButtonClickedListener() {
370            @Override
371            public void onBackButtonClicked() {
372                onBackPressed();
373            }
374        });
375
376        mIsLandscape = getResources().getConfiguration().orientation
377                == Configuration.ORIENTATION_LANDSCAPE;
378
379        final View floatingActionButtonContainer = findViewById(
380                R.id.floating_action_button_container);
381        ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
382        floatingActionButton.setOnClickListener(this);
383        mFloatingActionButtonController = new FloatingActionButtonController(this,
384                floatingActionButtonContainer, floatingActionButton);
385
386        ImageButton optionsMenuButton =
387                (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button);
388        optionsMenuButton.setOnClickListener(this);
389        mOverflowMenu = buildOptionsMenu(searchEditTextLayout);
390        optionsMenuButton.setOnTouchListener(mOverflowMenu.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
408        final boolean isLayoutRtl = DialerUtils.isRtl();
409        if (mIsLandscape) {
410            mSlideIn = AnimationUtils.loadAnimation(this,
411                    isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
412            mSlideOut = AnimationUtils.loadAnimation(this,
413                    isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
414        } else {
415            mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom);
416            mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom);
417        }
418
419        mSlideIn.setInterpolator(AnimUtils.EASE_IN);
420        mSlideOut.setInterpolator(AnimUtils.EASE_OUT);
421
422        mSlideOut.setAnimationListener(mSlideOutListener);
423
424        mParentLayout = (FrameLayout) findViewById(R.id.dialtacts_mainlayout);
425        mParentLayout.setOnDragListener(new LayoutOnDragListener());
426        floatingActionButtonContainer.getViewTreeObserver().addOnGlobalLayoutListener(
427                new ViewTreeObserver.OnGlobalLayoutListener() {
428                    @Override
429                    public void onGlobalLayout() {
430                        final ViewTreeObserver observer =
431                                floatingActionButtonContainer.getViewTreeObserver();
432                        if (!observer.isAlive()) {
433                            return;
434                        }
435                        observer.removeOnGlobalLayoutListener(this);
436                        int screenWidth = mParentLayout.getWidth();
437                        mFloatingActionButtonController.setScreenWidth(screenWidth);
438                        updateFloatingActionButtonControllerAlignment(false /* animate */);
439                    }
440                });
441
442        setupActivityOverlay();
443
444        mDialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper(this);
445        SmartDialPrefix.initializeNanpSettings(this);
446    }
447
448    private void setupActivityOverlay() {
449        final View activityOverlay = findViewById(R.id.activity_overlay);
450        activityOverlay.setOnTouchListener(new OnTouchListener() {
451            @Override
452            public boolean onTouch(View v, MotionEvent event) {
453                if (!mIsDialpadShown) {
454                    maybeExitSearchUi();
455                }
456                return false;
457            }
458        });
459    }
460
461    @Override
462    protected void onResume() {
463        super.onResume();
464        if (mFirstLaunch) {
465            displayFragment(getIntent());
466        } else if (!phoneIsInUse() && mInCallDialpadUp) {
467            hideDialpadFragment(false, true);
468            mInCallDialpadUp = false;
469        } else if (mShowDialpadOnResume) {
470            showDialpadFragment(false);
471            mShowDialpadOnResume = false;
472        }
473        mFirstLaunch = false;
474        prepareVoiceSearchButton();
475        mDialerDatabaseHelper.startSmartDialUpdateThread();
476        updateFloatingActionButtonControllerAlignment(false /* animate */);
477    }
478
479    @Override
480    protected void onPause() {
481        if (mClearSearchOnPause) {
482            hideDialpadAndSearchUi();
483            mClearSearchOnPause = false;
484        }
485        super.onPause();
486    }
487
488    @Override
489    protected void onSaveInstanceState(Bundle outState) {
490        super.onSaveInstanceState(outState);
491        outState.putString(KEY_SEARCH_QUERY, mSearchQuery);
492        outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch);
493        outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch);
494        outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch);
495        outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown);
496        mActionBarController.saveInstanceState(outState);
497    }
498
499    @Override
500    public void onAttachFragment(Fragment fragment) {
501        if (fragment instanceof DialpadFragment) {
502            mDialpadFragment = (DialpadFragment) fragment;
503            if (!mShowDialpadOnResume) {
504                final FragmentTransaction transaction = getFragmentManager().beginTransaction();
505                transaction.hide(mDialpadFragment);
506                transaction.commit();
507            }
508        } else if (fragment instanceof SmartDialSearchFragment) {
509            mSmartDialSearchFragment = (SmartDialSearchFragment) fragment;
510            mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this);
511        } else if (fragment instanceof SearchFragment) {
512            mRegularSearchFragment = (RegularSearchFragment) fragment;
513            mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this);
514        } else if (fragment instanceof ListsFragment) {
515            mListsFragment = (ListsFragment) fragment;
516            mListsFragment.addOnPageChangeListener(this);
517        }
518    }
519
520    protected void handleMenuSettings() {
521        final Intent intent = new Intent(this, DialerSettingsActivity.class);
522        startActivity(intent);
523    }
524
525    @Override
526    public void onClick(View view) {
527        switch (view.getId()) {
528            case R.id.floating_action_button:
529                if (!mIsDialpadShown) {
530                    mInCallDialpadUp = false;
531                    showDialpadFragment(true);
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                mOverflowMenu.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        mDialpadFragment.sendScreenView();
618
619        final FragmentTransaction ft = getFragmentManager().beginTransaction();
620        ft.show(mDialpadFragment);
621        ft.commit();
622
623        if (animate) {
624            mFloatingActionButtonController.scaleOut();
625        } else {
626            mFloatingActionButtonController.setVisible(false);
627        }
628        mActionBarController.onDialpadUp();
629
630        if (!isInSearchUi()) {
631            enterSearchUi(true /* isSmartDial */, mSearchQuery);
632        }
633    }
634
635    /**
636     * Callback from child DialpadFragment when the dialpad is shown.
637     */
638    public void onDialpadShown() {
639        if (mDialpadFragment.getAnimate()) {
640            mDialpadFragment.getView().startAnimation(mSlideIn);
641        } else {
642            mDialpadFragment.setYFraction(0);
643        }
644
645        updateSearchFragmentPosition();
646    }
647
648    /**
649     * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in
650     * a callback after the hide animation ends.
651     * @see #commitDialpadFragmentHide
652     */
653    public void hideDialpadFragment(boolean animate, boolean clearDialpad) {
654        if (mDialpadFragment == null) {
655            return;
656        }
657        if (clearDialpad) {
658            mDialpadFragment.clearDialpad();
659        }
660        if (!mIsDialpadShown) {
661            return;
662        }
663        mIsDialpadShown = false;
664        mDialpadFragment.setAnimate(animate);
665
666        updateSearchFragmentPosition();
667
668        updateFloatingActionButtonControllerAlignment(animate);
669        if (animate) {
670            mDialpadFragment.getView().startAnimation(mSlideOut);
671        } else {
672            commitDialpadFragmentHide();
673        }
674
675        mActionBarController.onDialpadDown();
676
677        if (isInSearchUi()) {
678            if (TextUtils.isEmpty(mSearchQuery)) {
679                exitSearchUi();
680            }
681        }
682    }
683
684    /**
685     * Finishes hiding the dialpad fragment after any animations are completed.
686     */
687    private void commitDialpadFragmentHide() {
688        final FragmentTransaction ft = getFragmentManager().beginTransaction();
689        ft.hide(mDialpadFragment);
690        ft.commit();
691
692        mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);
693    }
694
695    private void updateSearchFragmentPosition() {
696        SearchFragment fragment = null;
697        if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) {
698            fragment = mSmartDialSearchFragment;
699        } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) {
700            fragment = mRegularSearchFragment;
701        }
702        if (fragment != null && fragment.isVisible()) {
703            fragment.updatePosition(true /* animate */);
704        }
705    }
706
707    @Override
708    public boolean isInSearchUi() {
709        return mInDialpadSearch || mInRegularSearch;
710    }
711
712    @Override
713    public boolean hasSearchQuery() {
714        return !TextUtils.isEmpty(mSearchQuery);
715    }
716
717    @Override
718    public boolean shouldShowActionBar() {
719        return mListsFragment.shouldShowActionBar();
720    }
721
722    private void setNotInSearchUi() {
723        mInDialpadSearch = false;
724        mInRegularSearch = false;
725    }
726
727    private void hideDialpadAndSearchUi() {
728        if (mIsDialpadShown) {
729            hideDialpadFragment(false, true);
730        } else {
731            exitSearchUi();
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        final Menu menu = popupMenu.getMenu();
749        popupMenu.setOnMenuItemClickListener(this);
750        return popupMenu;
751    }
752
753    @Override
754    public boolean onCreateOptionsMenu(Menu menu) {
755        if (mPendingSearchViewQuery != null) {
756            mSearchView.setText(mPendingSearchViewQuery);
757            mPendingSearchViewQuery = null;
758        }
759        mActionBarController.restoreActionBarOffset();
760        return false;
761    }
762
763    /**
764     * Returns true if the intent is due to hitting the green send key (hardware call button:
765     * KEYCODE_CALL) while in a call.
766     *
767     * @param intent the intent that launched this activity
768     * @return true if the intent is due to hitting the green send key while in a call
769     */
770    private boolean isSendKeyWhileInCall(Intent intent) {
771        // If there is a call in progress and the user launched the dialer by hitting the call
772        // button, go straight to the in-call screen.
773        final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction());
774
775        if (callKey) {
776            getTelecomManager().showInCallScreen(false);
777            return true;
778        }
779
780        return false;
781    }
782
783    /**
784     * Sets the current tab based on the intent's request type
785     *
786     * @param intent Intent that contains information about which tab should be selected
787     */
788    private void displayFragment(Intent intent) {
789        // If we got here by hitting send and we're in call forward along to the in-call activity
790        if (isSendKeyWhileInCall(intent)) {
791            finish();
792            return;
793        }
794
795        if (mDialpadFragment != null) {
796            final boolean phoneIsInUse = phoneIsInUse();
797            if (phoneIsInUse || (intent.getData() !=  null && isDialIntent(intent))) {
798                mDialpadFragment.setStartedFromNewIntent(true);
799                if (phoneIsInUse && !mDialpadFragment.isVisible()) {
800                    mInCallDialpadUp = true;
801                }
802                showDialpadFragment(false);
803            }
804        }
805    }
806
807    @Override
808    public void onNewIntent(Intent newIntent) {
809        setIntent(newIntent);
810        displayFragment(newIntent);
811
812        invalidateOptionsMenu();
813    }
814
815    /** Returns true if the given intent contains a phone number to populate the dialer with */
816    private boolean isDialIntent(Intent intent) {
817        final String action = intent.getAction();
818        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
819            return true;
820        }
821        if (Intent.ACTION_VIEW.equals(action)) {
822            final Uri data = intent.getData();
823            if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) {
824                return true;
825            }
826        }
827        return false;
828    }
829
830    /**
831     * Returns an appropriate call origin for this Activity. May return null when no call origin
832     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
833     * for remembering the tab in which the user made a phone call, so the external app's DIAL
834     * request should not be counted.)
835     */
836    public String getCallOrigin() {
837        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
838    }
839
840    /**
841     * Shows the search fragment
842     */
843    private void enterSearchUi(boolean smartDialSearch, String query) {
844        if (getFragmentManager().isDestroyed()) {
845            // Weird race condition where fragment is doing work after the activity is destroyed
846            // due to talkback being on (b/10209937). Just return since we can't do any
847            // constructive here.
848            return;
849        }
850
851        if (DEBUG) {
852            Log.d(TAG, "Entering search UI - smart dial " + smartDialSearch);
853        }
854
855        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
856        if (mInDialpadSearch && mSmartDialSearchFragment != null) {
857            transaction.remove(mSmartDialSearchFragment);
858        } else if (mInRegularSearch && mRegularSearchFragment != null) {
859            transaction.remove(mRegularSearchFragment);
860        }
861
862        final String tag;
863        if (smartDialSearch) {
864            tag = TAG_SMARTDIAL_SEARCH_FRAGMENT;
865        } else {
866            tag = TAG_REGULAR_SEARCH_FRAGMENT;
867        }
868        mInDialpadSearch = smartDialSearch;
869        mInRegularSearch = !smartDialSearch;
870
871        SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag);
872        transaction.setCustomAnimations(android.R.animator.fade_in, 0);
873        if (fragment == null) {
874            if (smartDialSearch) {
875                fragment = new SmartDialSearchFragment();
876            } else {
877                fragment = new RegularSearchFragment();
878            }
879            transaction.add(R.id.dialtacts_frame, fragment, tag);
880        } else {
881            transaction.show(fragment);
882        }
883        // DialtactsActivity will provide the options menu
884        fragment.setHasOptionsMenu(false);
885        fragment.setShowEmptyListForNullQuery(true);
886        fragment.setQueryString(query, false /* delaySelection */);
887        transaction.commit();
888
889        mListsFragment.getView().animate().alpha(0).withLayer();
890    }
891
892    /**
893     * Hides the search fragment
894     */
895    private void exitSearchUi() {
896        // See related bug in enterSearchUI();
897        if (getFragmentManager().isDestroyed()) {
898            return;
899        }
900
901        mSearchView.setText(null);
902        mDialpadFragment.clearDialpad();
903        setNotInSearchUi();
904
905        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
906        if (mSmartDialSearchFragment != null) {
907            transaction.remove(mSmartDialSearchFragment);
908        }
909        if (mRegularSearchFragment != null) {
910            transaction.remove(mRegularSearchFragment);
911        }
912        transaction.commit();
913
914        mListsFragment.getView().animate().alpha(1).withLayer();
915        mActionBarController.onSearchUiExited();
916    }
917
918    /** Returns an Intent to launch Call Settings screen */
919    public static Intent getCallSettingsIntent() {
920        final Intent intent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS);
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                    (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()
930                            && mSmartDialSearchFragment.getAdapter().getCount() == 0)) {
931                exitSearchUi();
932            }
933            hideDialpadFragment(true, false);
934        } else if (isInSearchUi()) {
935            exitSearchUi();
936            DialerUtils.hideInputMethod(mParentLayout);
937        } else {
938            super.onBackPressed();
939        }
940    }
941
942    /**
943     * @return True if the search UI was exited, false otherwise
944     */
945    private boolean maybeExitSearchUi() {
946        if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) {
947            exitSearchUi();
948            DialerUtils.hideInputMethod(mParentLayout);
949            return true;
950        }
951        return false;
952    }
953
954    @Override
955    public void onDialpadQueryChanged(String query) {
956        if (mSmartDialSearchFragment != null) {
957            mSmartDialSearchFragment.setAddToContactNumber(query);
958        }
959        final String normalizedQuery = SmartDialNameMatcher.normalizeNumber(query,
960                SmartDialNameMatcher.LATIN_SMART_DIAL_MAP);
961
962        if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) {
963            if (DEBUG) {
964                Log.d(TAG, "onDialpadQueryChanged - new query: " + query);
965            }
966            if (mDialpadFragment == null || !mDialpadFragment.isVisible()) {
967                // This callback can happen if the dialpad fragment is recreated because of
968                // activity destruction. In that case, don't update the search view because
969                // that would bring the user back to the search fragment regardless of the
970                // previous state of the application. Instead, just return here and let the
971                // fragment manager correctly figure out whatever fragment was last displayed.
972                if (!TextUtils.isEmpty(normalizedQuery)) {
973                    mPendingSearchViewQuery = normalizedQuery;
974                }
975                return;
976            }
977            mSearchView.setText(normalizedQuery);
978        }
979    }
980
981    @Override
982    public void onListFragmentScrollStateChange(int scrollState) {
983        if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
984            hideDialpadFragment(true, false);
985            DialerUtils.hideInputMethod(mParentLayout);
986        }
987    }
988
989    @Override
990    public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount,
991                                     int totalItemCount) {
992        // TODO: No-op for now. This should eventually show/hide the actionBar based on
993        // interactions with the ListsFragments.
994    }
995
996    private boolean phoneIsInUse() {
997        return getTelecomManager().isInCall();
998    }
999
1000    public static Intent getAddNumberToContactIntent(CharSequence text) {
1001        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
1002        intent.putExtra(Intents.Insert.PHONE, text);
1003        intent.setType(Contacts.CONTENT_ITEM_TYPE);
1004        return intent;
1005    }
1006
1007    private boolean canIntentBeHandled(Intent intent) {
1008        final PackageManager packageManager = getPackageManager();
1009        final List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
1010                PackageManager.MATCH_DEFAULT_ONLY);
1011        return resolveInfo != null && resolveInfo.size() > 0;
1012    }
1013
1014    @Override
1015    public void showCallHistory() {
1016        // Use explicit CallLogActivity intent instead of ACTION_VIEW +
1017        // CONTENT_TYPE, so that we always open our call log from our dialer
1018        final Intent intent = new Intent(this, CallLogActivity.class);
1019        startActivity(intent);
1020    }
1021
1022    /**
1023     * Called when the user has long-pressed a contact tile to start a drag operation.
1024     */
1025    @Override
1026    public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) {
1027        if (mListsFragment.isPaneOpen()) {
1028            mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_SHOWN_ALPHA);
1029        }
1030        mListsFragment.showRemoveView(true);
1031    }
1032
1033    @Override
1034    public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {
1035    }
1036
1037    /**
1038     * Called when the user has released a contact tile after long-pressing it.
1039     */
1040    @Override
1041    public void onDragFinished(int x, int y) {
1042        if (mListsFragment.isPaneOpen()) {
1043            mActionBarController.setAlpha(ListsFragment.REMOVE_VIEW_HIDDEN_ALPHA);
1044        }
1045        mListsFragment.showRemoveView(false);
1046    }
1047
1048    @Override
1049    public void onDroppedOnRemove() {}
1050
1051    /**
1052     * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer
1053     * once it has been attached to the activity.
1054     */
1055    @Override
1056    public void setDragDropController(DragDropController dragController) {
1057        mDragDropController = dragController;
1058        mListsFragment.getRemoveView().setDragDropController(dragController);
1059    }
1060
1061    @Override
1062    public void onPickPhoneNumberAction(Uri dataUri) {
1063        // Specify call-origin so that users will see the previous tab instead of
1064        // CallLog screen (search UI will be automatically exited).
1065        PhoneNumberInteraction.startInteractionForPhoneCall(
1066                DialtactsActivity.this, dataUri, getCallOrigin());
1067        mClearSearchOnPause = true;
1068    }
1069
1070    @Override
1071    public void onCallNumberDirectly(String phoneNumber) {
1072        onCallNumberDirectly(phoneNumber, false /* isVideoCall */);
1073    }
1074
1075    @Override
1076    public void onCallNumberDirectly(String phoneNumber, boolean isVideoCall) {
1077        Intent intent = isVideoCall ?
1078                CallUtil.getVideoCallIntent(phoneNumber, getCallOrigin()) :
1079                CallUtil.getCallIntent(phoneNumber, getCallOrigin());
1080        DialerUtils.startActivityWithErrorToast(this, intent);
1081        mClearSearchOnPause = true;
1082    }
1083
1084    @Override
1085    public void onShortcutIntentCreated(Intent intent) {
1086        Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
1087    }
1088
1089    @Override
1090    public void onHomeInActionBarSelected() {
1091        exitSearchUi();
1092    }
1093
1094    @Override
1095    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
1096        position = mListsFragment.getRtlPosition(position);
1097        // Only scroll the button when the first tab is selected. The button should scroll from
1098        // the middle to right position only on the transition from the first tab to the second
1099        // tab.
1100        // If the app is in RTL mode, we need to check against the second tab, rather than the
1101        // first. This is because if we are scrolling between the first and second tabs, the
1102        // viewpager will report that the starting tab position is 1 rather than 0, due to the
1103        // reversal of the order of the tabs.
1104        final boolean isLayoutRtl = DialerUtils.isRtl();
1105        final boolean shouldScrollButton = position == (isLayoutRtl
1106                ? ListsFragment.TAB_INDEX_RECENTS : ListsFragment.TAB_INDEX_SPEED_DIAL);
1107        if (shouldScrollButton && !mIsLandscape) {
1108            mFloatingActionButtonController.onPageScrolled(
1109                    isLayoutRtl ? 1 - positionOffset : positionOffset);
1110        } else if (position != ListsFragment.TAB_INDEX_SPEED_DIAL) {
1111            mFloatingActionButtonController.onPageScrolled(1);
1112        }
1113    }
1114
1115    @Override
1116    public void onPageSelected(int position) {
1117        position = mListsFragment.getRtlPosition(position);
1118        mCurrentTabPosition = position;
1119    }
1120
1121    @Override
1122    public void onPageScrollStateChanged(int state) {
1123    }
1124
1125    private TelephonyManager getTelephonyManager() {
1126        return (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
1127    }
1128
1129    private TelecomManager getTelecomManager() {
1130        return (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
1131    }
1132
1133    @Override
1134    public boolean isActionBarShowing() {
1135        return mActionBarController.isActionBarShowing();
1136    }
1137
1138    public boolean isDialpadShown() {
1139        return mIsDialpadShown;
1140    }
1141
1142    @Override
1143    public int getActionBarHideOffset() {
1144        return getActionBar().getHideOffset();
1145    }
1146
1147    @Override
1148    public int getActionBarHeight() {
1149        return mActionBarHeight;
1150    }
1151
1152    @Override
1153    public void setActionBarHideOffset(int hideOffset) {
1154        mActionBarController.setHideOffset(hideOffset);
1155    }
1156
1157    /**
1158     * Updates controller based on currently known information.
1159     *
1160     * @param animate Whether or not to animate the transition.
1161     */
1162    private void updateFloatingActionButtonControllerAlignment(boolean animate) {
1163        int align = (!mIsLandscape && mCurrentTabPosition == ListsFragment.TAB_INDEX_SPEED_DIAL) ?
1164                FloatingActionButtonController.ALIGN_MIDDLE :
1165                        FloatingActionButtonController.ALIGN_END;
1166        mFloatingActionButtonController.align(align, 0 /* offsetX */, 0 /* offsetY */, animate);
1167    }
1168}
1169