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