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